mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 22:55:23 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 832966016d | |||
| 33767c4840 | |||
| dfb1aee921 | |||
| 82acf110ab | |||
| bbf61996a9 | |||
| 766c7fba67 | |||
| c4535c9948 | |||
| 56edb7db36 | |||
| e24b67e588 | |||
| 76e4237b92 | |||
| 31d9b50985 | |||
| 1a387d78f8 | |||
| 7f338c572d | |||
| 206fd92537 | |||
| d4e458b02a | |||
| eb9a80fe1f | |||
| 6296ac656b | |||
| 59495ff26a | |||
| 3acb421f68 | |||
| 01cd296b07 | |||
| 3c3112d33e | |||
| 7b491e4218 | |||
| 73782b260d | |||
| 13f9b5716d | |||
| 8a0c1e7ce5 | |||
| d03498175d | |||
| 9b1817df6e | |||
| 50386918ff | |||
| 50a63bc48a |
+44
-1
@@ -1,5 +1,44 @@
|
||||
# Rocksdb Change Log
|
||||
## Unreleased
|
||||
## 7.7.8 (11/27/2022)
|
||||
### Bug Fixes
|
||||
* Fix failed memtable flush retry bug that could cause wrongly ordered updates, which would surface to writers as `Status::Corruption` in case of `force_consistency_checks=true` (default). It affects use cases that enable both parallel flush (`max_background_flushes > 1` or `max_background_jobs >= 8`) and non-default memtable count (`max_write_buffer_number > 2`).
|
||||
* Tiered Storage: fixed excessive keys written to penultimate level in non-debug builds.
|
||||
* Fixed a regression in iterator where range tombstones after `iterate_upper_bound` is processed.
|
||||
|
||||
## 7.7.7 (11/15/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a regression in scan for async_io. During seek, valid buffers were getting cleared causing a regression.
|
||||
|
||||
## 7.7.6 (11/03/2022)
|
||||
### Bug Fixes
|
||||
* Fix memory corruption error in scans if async_io is enabled. Memory corruption happened if there is IOError while reading the data leading to empty buffer and other buffer already in progress of async read goes again for reading.
|
||||
|
||||
## 7.7.5 (10/28/2022)
|
||||
### Bug Fixes
|
||||
* Fixed an iterator performance regression for delete range users when scanning through a consecutive sequence of range tombstones (#10877).
|
||||
|
||||
## 7.7.4 (10/28/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a case of calling malloc_usable_size on result of operator new[].
|
||||
|
||||
## 7.7.3 (10/11/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a memory safety bug in experimental HyperClockCache (#10768)
|
||||
|
||||
## 7.7.2 (10/05/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a bug in iterator refresh that was not freeing up SuperVersion, which could cause excessive resource pinniung (#10770).
|
||||
* Fixed a bug where RocksDB could be doing compaction endlessly when allow_ingest_behind is true and the bottommost level is not filled (#10767).
|
||||
|
||||
### Behavior Changes
|
||||
* Sanitize min_write_buffer_number_to_merge to 1 if atomic flush is enabled to prevent unexpected data loss when WAL is disabled in a multi-column-family setting (#10773).
|
||||
|
||||
## 7.7.1 (09/26/2022)
|
||||
### Bug Fixes
|
||||
* Fixed an optimistic transaction validation bug caused by DBImpl::GetLatestSequenceForKey() returning non-latest seq for merge (#10724).
|
||||
* Fixed a bug in iterator refresh which could segfault for DeleteRange users (#10739).
|
||||
|
||||
## 7.7.0 (09/18/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a hang when an operation such as `GetLiveFiles` or `CreateNewBackup` is asked to trigger and wait for memtable flush on a read-only DB. Such indirect requests for memtable flush are now ignored on a read-only DB.
|
||||
* Fixed bug where `FlushWAL(true /* sync */)` (used by `GetLiveFilesStorageInfo()`, which is used by checkpoint and backup) could cause parallel writes at the tail of a WAL file to never be synced.
|
||||
@@ -10,6 +49,10 @@
|
||||
* Fix a bug in key range overlap checking with concurrent compactions when user-defined timestamp is enabled. User-defined timestamps should be EXCLUDED when checking if two ranges overlap.
|
||||
* Fixed a bug where the blob cache prepopulating logic did not consider the secondary cache (see #10603).
|
||||
* Fixed the rocksdb.num.sst.read.per.level, rocksdb.num.index.and.filter.blocks.read.per.level and rocksdb.num.level.read.per.multiget stats in the MultiGet coroutines
|
||||
* Fix a bug in io_uring_prep_cancel in AbortIO API for posix which expects sqe->addr to match with read request submitted and wrong paramter was being passed.
|
||||
* Fixed a regression in iterator performance when the entire DB is a single memtable introduced in #10449. The fix is in #10705 and #10716.
|
||||
* Fix a bug in io_uring_prep_cancel in AbortIO API for posix which expects sqe->addr to match with read request submitted and wrong paramter was being passed.
|
||||
* Fixed a regression in iterator performance when the entire DB is a single memtable introduced in #10449. The fix is in #10705 and #10716.
|
||||
|
||||
### Public API changes
|
||||
* Add `rocksdb_column_family_handle_get_id`, `rocksdb_column_family_handle_get_name` to get name, id of column family in C API
|
||||
|
||||
Vendored
+39
-28
@@ -23,6 +23,12 @@ namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace hyper_clock_cache {
|
||||
|
||||
inline uint64_t GetRefcount(uint64_t meta) {
|
||||
return ((meta >> ClockHandle::kAcquireCounterShift) -
|
||||
(meta >> ClockHandle::kReleaseCounterShift)) &
|
||||
ClockHandle::kCounterMask;
|
||||
}
|
||||
|
||||
static_assert(sizeof(ClockHandle) == 64U,
|
||||
"Expecting size / alignment with common cache line size");
|
||||
|
||||
@@ -49,6 +55,7 @@ ClockHandleTable::~ClockHandleTable() {
|
||||
break;
|
||||
case ClockHandle::kStateInvisible: // rare but possible
|
||||
case ClockHandle::kStateVisible:
|
||||
assert(GetRefcount(h.meta) == 0);
|
||||
h.FreeData();
|
||||
#ifndef NDEBUG
|
||||
Rollback(h.hash, &h);
|
||||
@@ -562,10 +569,7 @@ bool ClockHandleTable::Release(ClockHandle* h, bool useful,
|
||||
}
|
||||
// Take ownership if no refs
|
||||
do {
|
||||
uint64_t refcount = ((old_meta >> ClockHandle::kAcquireCounterShift) -
|
||||
(old_meta >> ClockHandle::kReleaseCounterShift)) &
|
||||
ClockHandle::kCounterMask;
|
||||
if (refcount != 0) {
|
||||
if (GetRefcount(old_meta) != 0) {
|
||||
// Not last ref at some point in time during this Release call
|
||||
// Correct for possible (but rare) overflow
|
||||
CorrectNearOverflow(old_meta, h->meta);
|
||||
@@ -622,6 +626,8 @@ void ClockHandleTable::Ref(ClockHandle& h) {
|
||||
|
||||
assert((old_meta >> ClockHandle::kStateShift) &
|
||||
ClockHandle::kStateShareableBit);
|
||||
// Must have already had a reference
|
||||
assert(GetRefcount(old_meta) > 0);
|
||||
(void)old_meta;
|
||||
}
|
||||
|
||||
@@ -671,10 +677,7 @@ void ClockHandleTable::Erase(const CacheKeyBytes& key, uint32_t hash) {
|
||||
old_meta &= ~(uint64_t{ClockHandle::kStateVisibleBit}
|
||||
<< ClockHandle::kStateShift);
|
||||
for (;;) {
|
||||
uint64_t refcount =
|
||||
((old_meta >> ClockHandle::kAcquireCounterShift) -
|
||||
(old_meta >> ClockHandle::kReleaseCounterShift)) &
|
||||
ClockHandle::kCounterMask;
|
||||
uint64_t refcount = GetRefcount(old_meta);
|
||||
assert(refcount > 0);
|
||||
if (refcount > 1) {
|
||||
// Not last ref at some point in time during this Erase call
|
||||
@@ -683,8 +686,10 @@ void ClockHandleTable::Erase(const CacheKeyBytes& key, uint32_t hash) {
|
||||
std::memory_order_release);
|
||||
break;
|
||||
} else if (h->meta.compare_exchange_weak(
|
||||
old_meta, uint64_t{ClockHandle::kStateConstruction}
|
||||
<< ClockHandle::kStateShift)) {
|
||||
old_meta,
|
||||
uint64_t{ClockHandle::kStateConstruction}
|
||||
<< ClockHandle::kStateShift,
|
||||
std::memory_order_acq_rel)) {
|
||||
// Took ownership
|
||||
assert(hash == h->hash);
|
||||
// TODO? Delay freeing?
|
||||
@@ -740,20 +745,32 @@ void ClockHandleTable::ConstApplyToEntriesRange(
|
||||
for (uint32_t i = index_begin; i < index_end; i++) {
|
||||
ClockHandle& h = array_[i];
|
||||
|
||||
// Note: to avoid using compare_exchange, we have to be extra careful.
|
||||
uint64_t old_meta = h.meta.load(std::memory_order_relaxed);
|
||||
// Check if it's an entry visible to lookups
|
||||
if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
|
||||
// Increment acquire counter
|
||||
// Increment acquire counter. Note: it's possible that the entry has
|
||||
// completely changed since we loaded old_meta, but incrementing acquire
|
||||
// count is always safe. (Similar to optimistic Lookup here.)
|
||||
old_meta = h.meta.fetch_add(ClockHandle::kAcquireIncrement,
|
||||
std::memory_order_acquire);
|
||||
// Double-check
|
||||
if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
|
||||
func(h);
|
||||
// Check whether we actually acquired a reference.
|
||||
if ((old_meta >> ClockHandle::kStateShift) &
|
||||
ClockHandle::kStateShareableBit) {
|
||||
// Apply func if appropriate
|
||||
if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
|
||||
func(h);
|
||||
}
|
||||
// Pretend we never took the reference
|
||||
h.meta.fetch_sub(ClockHandle::kAcquireIncrement,
|
||||
std::memory_order_release);
|
||||
// No net change, so don't need to check for overflow
|
||||
} else {
|
||||
// For other states, incrementing the acquire counter has no effect
|
||||
// so we don't need to undo it. Furthermore, we cannot safely undo
|
||||
// it because we did not acquire a read reference to lock the
|
||||
// entry in a Shareable state.
|
||||
}
|
||||
// Pretend we never took the reference
|
||||
h.meta.fetch_sub(ClockHandle::kAcquireIncrement,
|
||||
std::memory_order_release);
|
||||
// No net change, so don't need to check for overflow
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -763,12 +780,9 @@ void ClockHandleTable::EraseUnRefEntries() {
|
||||
ClockHandle& h = array_[i];
|
||||
|
||||
uint64_t old_meta = h.meta.load(std::memory_order_relaxed);
|
||||
uint64_t refcount = ((old_meta >> ClockHandle::kAcquireCounterShift) -
|
||||
(old_meta >> ClockHandle::kReleaseCounterShift)) &
|
||||
ClockHandle::kCounterMask;
|
||||
if (old_meta & (uint64_t{ClockHandle::kStateShareableBit}
|
||||
<< ClockHandle::kStateShift) &&
|
||||
refcount == 0 &&
|
||||
GetRefcount(old_meta) == 0 &&
|
||||
h.meta.compare_exchange_strong(old_meta,
|
||||
uint64_t{ClockHandle::kStateConstruction}
|
||||
<< ClockHandle::kStateShift,
|
||||
@@ -877,13 +891,12 @@ void ClockHandleTable::Evict(size_t requested_charge, size_t* freed_charge,
|
||||
// Only clock update entries with no outstanding refs
|
||||
continue;
|
||||
}
|
||||
if (!(meta >> ClockHandle::kStateShift &
|
||||
if (!((meta >> ClockHandle::kStateShift) &
|
||||
ClockHandle::kStateShareableBit)) {
|
||||
// Only clock update Shareable entries
|
||||
continue;
|
||||
}
|
||||
// ModTableSize(old_clock_pointer + i));
|
||||
if (meta >> ClockHandle::kStateShift == ClockHandle::kStateVisible &&
|
||||
if ((meta >> ClockHandle::kStateShift == ClockHandle::kStateVisible) &&
|
||||
acquire_count > 0) {
|
||||
// Decrement clock
|
||||
uint64_t new_count = std::min(acquire_count - 1,
|
||||
@@ -1101,9 +1114,7 @@ size_t ClockCacheShard::GetPinnedUsage() const {
|
||||
table_.ConstApplyToEntriesRange(
|
||||
[&table_pinned_usage, charge_metadata](const ClockHandle& h) {
|
||||
uint64_t meta = h.meta.load(std::memory_order_relaxed);
|
||||
uint64_t refcount = ((meta >> ClockHandle::kAcquireCounterShift) -
|
||||
(meta >> ClockHandle::kReleaseCounterShift)) &
|
||||
ClockHandle::kCounterMask;
|
||||
uint64_t refcount = GetRefcount(meta);
|
||||
// Holding one ref for ConstApplyToEntriesRange
|
||||
assert(refcount > 0);
|
||||
if (refcount > 1) {
|
||||
|
||||
Vendored
+4
-5
@@ -382,7 +382,7 @@ Status LRUCacheShard::InsertItem(LRUHandle* e, Cache::Handle** handle,
|
||||
last_reference_list.push_back(e);
|
||||
} else {
|
||||
if (free_handle_on_fail) {
|
||||
delete[] reinterpret_cast<char*>(e);
|
||||
free(e);
|
||||
*handle = nullptr;
|
||||
}
|
||||
s = Status::MemoryLimit("Insert failed due to LRU cache being full.");
|
||||
@@ -561,8 +561,7 @@ Cache::Handle* LRUCacheShard::Lookup(
|
||||
secondary_cache_->Lookup(key, create_cb, wait, found_dummy_entry,
|
||||
is_in_sec_cache);
|
||||
if (secondary_handle != nullptr) {
|
||||
e = reinterpret_cast<LRUHandle*>(
|
||||
new char[sizeof(LRUHandle) - 1 + key.size()]);
|
||||
e = static_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
|
||||
|
||||
e->flags = 0;
|
||||
e->SetSecondaryCacheCompatible(true);
|
||||
@@ -684,8 +683,8 @@ Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
|
||||
// Allocate the memory here outside of the mutex.
|
||||
// If the cache is full, we'll have to release it.
|
||||
// It shouldn't happen very often though.
|
||||
LRUHandle* e = reinterpret_cast<LRUHandle*>(
|
||||
new char[sizeof(LRUHandle) - 1 + key.size()]);
|
||||
LRUHandle* e =
|
||||
static_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
|
||||
|
||||
e->value = value;
|
||||
e->flags = 0;
|
||||
|
||||
Vendored
+2
-1
@@ -241,7 +241,8 @@ struct LRUHandle {
|
||||
(*info_.helper->del_cb)(key(), value);
|
||||
}
|
||||
}
|
||||
delete[] reinterpret_cast<char*>(this);
|
||||
|
||||
free(this);
|
||||
}
|
||||
|
||||
inline size_t CalcuMetaCharge(
|
||||
|
||||
+50
-29
@@ -45,6 +45,7 @@ void ArenaWrappedDBIter::Init(
|
||||
sv_number_ = version_number;
|
||||
read_options_ = read_options;
|
||||
allow_refresh_ = allow_refresh;
|
||||
memtable_range_tombstone_iter_ = nullptr;
|
||||
}
|
||||
|
||||
Status ArenaWrappedDBIter::Refresh() {
|
||||
@@ -58,48 +59,68 @@ Status ArenaWrappedDBIter::Refresh() {
|
||||
uint64_t cur_sv_number = cfd_->GetSuperVersionNumber();
|
||||
TEST_SYNC_POINT("ArenaWrappedDBIter::Refresh:1");
|
||||
TEST_SYNC_POINT("ArenaWrappedDBIter::Refresh:2");
|
||||
auto reinit_internal_iter = [&]() {
|
||||
Env* env = db_iter_->env();
|
||||
db_iter_->~DBIter();
|
||||
arena_.~Arena();
|
||||
new (&arena_) Arena();
|
||||
|
||||
SuperVersion* sv = cfd_->GetReferencedSuperVersion(db_impl_);
|
||||
SequenceNumber latest_seq = db_impl_->GetLatestSequenceNumber();
|
||||
if (read_callback_) {
|
||||
read_callback_->Refresh(latest_seq);
|
||||
}
|
||||
Init(env, read_options_, *(cfd_->ioptions()), sv->mutable_cf_options,
|
||||
sv->current, latest_seq,
|
||||
sv->mutable_cf_options.max_sequential_skip_in_iterations,
|
||||
cur_sv_number, read_callback_, db_impl_, cfd_, expose_blob_index_,
|
||||
allow_refresh_);
|
||||
|
||||
InternalIterator* internal_iter = db_impl_->NewInternalIterator(
|
||||
read_options_, cfd_, sv, &arena_, latest_seq,
|
||||
/* allow_unprepared_value */ true, /* db_iter */ this);
|
||||
SetIterUnderDBIter(internal_iter);
|
||||
};
|
||||
while (true) {
|
||||
if (sv_number_ != cur_sv_number) {
|
||||
Env* env = db_iter_->env();
|
||||
db_iter_->~DBIter();
|
||||
arena_.~Arena();
|
||||
new (&arena_) Arena();
|
||||
|
||||
SuperVersion* sv = cfd_->GetReferencedSuperVersion(db_impl_);
|
||||
SequenceNumber latest_seq = db_impl_->GetLatestSequenceNumber();
|
||||
if (read_callback_) {
|
||||
read_callback_->Refresh(latest_seq);
|
||||
}
|
||||
Init(env, read_options_, *(cfd_->ioptions()), sv->mutable_cf_options,
|
||||
sv->current, latest_seq,
|
||||
sv->mutable_cf_options.max_sequential_skip_in_iterations,
|
||||
cur_sv_number, read_callback_, db_impl_, cfd_, expose_blob_index_,
|
||||
allow_refresh_);
|
||||
|
||||
InternalIterator* internal_iter = db_impl_->NewInternalIterator(
|
||||
read_options_, cfd_, sv, &arena_, latest_seq,
|
||||
/* allow_unprepared_value */ true, /* db_iter */ this);
|
||||
SetIterUnderDBIter(internal_iter);
|
||||
reinit_internal_iter();
|
||||
break;
|
||||
} else {
|
||||
SequenceNumber latest_seq = db_impl_->GetLatestSequenceNumber();
|
||||
// Refresh range-tombstones in MemTable
|
||||
if (!read_options_.ignore_range_deletions) {
|
||||
assert(memtable_range_tombstone_iter_ != nullptr);
|
||||
if (memtable_range_tombstone_iter_ != nullptr) {
|
||||
SuperVersion* sv = cfd_->GetThreadLocalSuperVersion(db_impl_);
|
||||
auto t = sv->mem->NewRangeTombstoneIterator(
|
||||
read_options_, latest_seq, false /* immutable_memtable */);
|
||||
delete *memtable_range_tombstone_iter_;
|
||||
if (t == nullptr || t->empty()) {
|
||||
*memtable_range_tombstone_iter_ = nullptr;
|
||||
SuperVersion* sv = cfd_->GetThreadLocalSuperVersion(db_impl_);
|
||||
TEST_SYNC_POINT_CALLBACK("ArenaWrappedDBIter::Refresh:SV", nullptr);
|
||||
auto t = sv->mem->NewRangeTombstoneIterator(
|
||||
read_options_, latest_seq, false /* immutable_memtable */);
|
||||
if (!t || t->empty()) {
|
||||
// If memtable_range_tombstone_iter_ points to a non-empty tombstone
|
||||
// iterator, then it means sv->mem is not the memtable that
|
||||
// memtable_range_tombstone_iter_ points to, so SV must have changed
|
||||
// after the sv_number_ != cur_sv_number check above. We will fall
|
||||
// back to re-init the InternalIterator, and the tombstone iterator
|
||||
// will be freed during db_iter destruction there.
|
||||
if (memtable_range_tombstone_iter_) {
|
||||
assert(!*memtable_range_tombstone_iter_ ||
|
||||
sv_number_ != cfd_->GetSuperVersionNumber());
|
||||
}
|
||||
delete t;
|
||||
} else { // current mutable memtable has range tombstones
|
||||
if (!memtable_range_tombstone_iter_) {
|
||||
delete t;
|
||||
db_impl_->ReturnAndCleanupSuperVersion(cfd_, sv);
|
||||
// The memtable under DBIter did not have range tombstone before
|
||||
// refresh.
|
||||
reinit_internal_iter();
|
||||
break;
|
||||
} else {
|
||||
delete *memtable_range_tombstone_iter_;
|
||||
*memtable_range_tombstone_iter_ = new TruncatedRangeDelIterator(
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator>(t),
|
||||
&cfd_->internal_comparator(), nullptr, nullptr);
|
||||
}
|
||||
cfd_->ReturnThreadLocalSuperVersion(sv);
|
||||
}
|
||||
db_impl_->ReturnAndCleanupSuperVersion(cfd_, sv);
|
||||
}
|
||||
// Refresh latest sequence number
|
||||
db_iter_->set_sequence(latest_seq);
|
||||
|
||||
@@ -107,6 +107,8 @@ class ArenaWrappedDBIter : public Iterator {
|
||||
ReadCallback* read_callback_;
|
||||
bool expose_blob_index_ = false;
|
||||
bool allow_refresh_ = true;
|
||||
// If this is nullptr, it means the mutable memtable does not contain range
|
||||
// tombstone when added under this DBIter.
|
||||
TruncatedRangeDelIterator** memtable_range_tombstone_iter_ = nullptr;
|
||||
};
|
||||
|
||||
|
||||
+17
-1
@@ -233,6 +233,21 @@ ColumnFamilyOptions SanitizeOptions(const ImmutableDBOptions& db_options,
|
||||
result.min_write_buffer_number_to_merge = 1;
|
||||
}
|
||||
|
||||
if (db_options.atomic_flush && result.min_write_buffer_number_to_merge > 1) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options.logger,
|
||||
"Currently, if atomic_flush is true, then triggering flush for any "
|
||||
"column family internally (non-manual flush) will trigger flushing "
|
||||
"all column families even if the number of memtables is smaller "
|
||||
"min_write_buffer_number_to_merge. Therefore, configuring "
|
||||
"min_write_buffer_number_to_merge > 1 is not compatible and should "
|
||||
"be satinized to 1. Not doing so will lead to data loss and "
|
||||
"inconsistent state across multiple column families when WAL is "
|
||||
"disabled, which is a common setting for atomic flush");
|
||||
|
||||
result.min_write_buffer_number_to_merge = 1;
|
||||
}
|
||||
|
||||
if (result.num_levels < 1) {
|
||||
result.num_levels = 1;
|
||||
}
|
||||
@@ -1144,7 +1159,8 @@ Status ColumnFamilyData::RangesOverlapWithMemtables(
|
||||
MergeIteratorBuilder merge_iter_builder(&internal_comparator_, &arena);
|
||||
merge_iter_builder.AddIterator(
|
||||
super_version->mem->NewIterator(read_opts, &arena));
|
||||
super_version->imm->AddIterators(read_opts, &merge_iter_builder);
|
||||
super_version->imm->AddIterators(read_opts, &merge_iter_builder,
|
||||
false /* add_range_tombstone_iter */);
|
||||
ScopedArenaIterator memtable_iter(merge_iter_builder.Finish());
|
||||
|
||||
auto read_seq = super_version->current->version_set()->LastSequence();
|
||||
|
||||
@@ -1086,7 +1086,9 @@ void CompactionIterator::DecideOutputLevel() {
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput.context",
|
||||
&context);
|
||||
output_to_penultimate_level_ = context.output_to_penultimate_level;
|
||||
#endif /* !NDEBUG */
|
||||
#else
|
||||
output_to_penultimate_level_ = false;
|
||||
#endif // NDEBUG
|
||||
|
||||
// if the key is newer than the cutoff sequence or within the earliest
|
||||
// snapshot, it should output to the penultimate level.
|
||||
|
||||
@@ -7958,6 +7958,42 @@ TEST_F(DBCompactionTest, BottomPriCompactionCountsTowardConcurrencyLimit) {
|
||||
compact_range_thread.join();
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, BottommostFileCompactionAllowIngestBehind) {
|
||||
// allow_ingest_behind prevents seqnum zeroing, and could cause
|
||||
// compaction loop with reason kBottommostFiles.
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.allow_ingest_behind = true;
|
||||
options.comparator = BytewiseComparator();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
WriteOptions write_opts;
|
||||
ASSERT_OK(db_->Put(write_opts, "infinite", "compaction loop"));
|
||||
ASSERT_OK(db_->Put(write_opts, "infinite", "loop"));
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_OK(db_->Put(write_opts, "bumpseqnum", ""));
|
||||
ASSERT_OK(Flush());
|
||||
auto snapshot = db_->GetSnapshot();
|
||||
// Bump up oldest_snapshot_seqnum_ in VersionStorageInfo.
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
bool compacted = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"LevelCompactionPicker::PickCompaction:Return", [&](void* /* arg */) {
|
||||
// There should not be a compaction.
|
||||
compacted = true;
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
// Wait for compaction to be scheduled.
|
||||
env_->SleepForMicroseconds(2000000);
|
||||
ASSERT_FALSE(compacted);
|
||||
// The following assert can be used to check for compaction loop:
|
||||
// it used to wait forever before the fix.
|
||||
// ASSERT_OK(dbfull()->TEST_WaitForCompact(true /* wait_unscheduled */));
|
||||
}
|
||||
|
||||
#endif // !defined(ROCKSDB_LITE)
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
+28
-24
@@ -1802,32 +1802,33 @@ InternalIterator* DBImpl::NewInternalIterator(
|
||||
MergeIteratorBuilder merge_iter_builder(
|
||||
&cfd->internal_comparator(), arena,
|
||||
!read_options.total_order_seek &&
|
||||
super_version->mutable_cf_options.prefix_extractor != nullptr);
|
||||
// Collect iterator for mutable mem
|
||||
merge_iter_builder.AddIterator(
|
||||
super_version->mem->NewIterator(read_options, arena));
|
||||
super_version->mutable_cf_options.prefix_extractor != nullptr,
|
||||
read_options.iterate_upper_bound);
|
||||
// Collect iterator for mutable memtable
|
||||
auto mem_iter = super_version->mem->NewIterator(read_options, arena);
|
||||
Status s;
|
||||
if (!read_options.ignore_range_deletions) {
|
||||
TruncatedRangeDelIterator* mem_tombstone_iter = nullptr;
|
||||
auto range_del_iter = super_version->mem->NewRangeTombstoneIterator(
|
||||
read_options, sequence, false /* immutable_memtable */);
|
||||
if (range_del_iter == nullptr || range_del_iter->empty()) {
|
||||
delete range_del_iter;
|
||||
merge_iter_builder.AddRangeTombstoneIterator(nullptr);
|
||||
} else {
|
||||
merge_iter_builder.AddRangeTombstoneIterator(
|
||||
new TruncatedRangeDelIterator(
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator>(range_del_iter),
|
||||
&cfd->ioptions()->internal_comparator, nullptr /* smallest */,
|
||||
nullptr /* largest */));
|
||||
mem_tombstone_iter = new TruncatedRangeDelIterator(
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator>(range_del_iter),
|
||||
&cfd->ioptions()->internal_comparator, nullptr /* smallest */,
|
||||
nullptr /* largest */);
|
||||
}
|
||||
merge_iter_builder.AddPointAndTombstoneIterator(mem_iter,
|
||||
mem_tombstone_iter);
|
||||
} else {
|
||||
merge_iter_builder.AddIterator(mem_iter);
|
||||
}
|
||||
|
||||
// Collect all needed child iterators for immutable memtables
|
||||
if (s.ok()) {
|
||||
super_version->imm->AddIterators(read_options, &merge_iter_builder);
|
||||
if (!read_options.ignore_range_deletions) {
|
||||
s = super_version->imm->AddRangeTombstoneIterators(read_options, arena,
|
||||
merge_iter_builder);
|
||||
}
|
||||
super_version->imm->AddIterators(read_options, &merge_iter_builder,
|
||||
!read_options.ignore_range_deletions);
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::NewInternalIterator:StatusCallback", &s);
|
||||
if (s.ok()) {
|
||||
@@ -3662,14 +3663,16 @@ void DBImpl::ReleaseSnapshot(const Snapshot* s) {
|
||||
if (oldest_snapshot > bottommost_files_mark_threshold_) {
|
||||
CfdList cf_scheduled;
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
cfd->current()->storage_info()->UpdateOldestSnapshot(oldest_snapshot);
|
||||
if (!cfd->current()
|
||||
->storage_info()
|
||||
->BottommostFilesMarkedForCompaction()
|
||||
.empty()) {
|
||||
SchedulePendingCompaction(cfd);
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
cf_scheduled.push_back(cfd);
|
||||
if (!cfd->ioptions()->allow_ingest_behind) {
|
||||
cfd->current()->storage_info()->UpdateOldestSnapshot(oldest_snapshot);
|
||||
if (!cfd->current()
|
||||
->storage_info()
|
||||
->BottommostFilesMarkedForCompaction()
|
||||
.empty()) {
|
||||
SchedulePendingCompaction(cfd);
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
cf_scheduled.push_back(cfd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3678,7 +3681,8 @@ void DBImpl::ReleaseSnapshot(const Snapshot* s) {
|
||||
// mutex might be unlocked during the loop, making the result inaccurate.
|
||||
SequenceNumber new_bottommost_files_mark_threshold = kMaxSequenceNumber;
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (CfdListContains(cf_scheduled, cfd)) {
|
||||
if (CfdListContains(cf_scheduled, cfd) ||
|
||||
cfd->ioptions()->allow_ingest_behind) {
|
||||
continue;
|
||||
}
|
||||
new_bottommost_files_mark_threshold = std::min(
|
||||
|
||||
@@ -3756,9 +3756,11 @@ void DBImpl::InstallSuperVersionAndScheduleWork(
|
||||
// triggered soon anyway.
|
||||
bottommost_files_mark_threshold_ = kMaxSequenceNumber;
|
||||
for (auto* my_cfd : *versions_->GetColumnFamilySet()) {
|
||||
bottommost_files_mark_threshold_ = std::min(
|
||||
bottommost_files_mark_threshold_,
|
||||
my_cfd->current()->storage_info()->bottommost_files_mark_threshold());
|
||||
if (!my_cfd->ioptions()->allow_ingest_behind) {
|
||||
bottommost_files_mark_threshold_ = std::min(
|
||||
bottommost_files_mark_threshold_,
|
||||
my_cfd->current()->storage_info()->bottommost_files_mark_threshold());
|
||||
}
|
||||
}
|
||||
|
||||
// Whenever we install new SuperVersion, we might need to issue new flushes or
|
||||
|
||||
@@ -89,6 +89,7 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
if (iter_.iter()) {
|
||||
iter_.iter()->SetPinnedItersMgr(&pinned_iters_mgr_);
|
||||
}
|
||||
status_.PermitUncheckedError();
|
||||
assert(timestamp_size_ == user_comparator_.timestamp_size());
|
||||
}
|
||||
|
||||
|
||||
@@ -3232,6 +3232,28 @@ TEST_F(DBIteratorTest, BackwardIterationOnInplaceUpdateMemtable) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest, IteratorRefreshReturnSV) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "a", "z"));
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(ReadOptions())};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"ArenaWrappedDBIter::Refresh:SV", [&](void*) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "dummy", "new SV"));
|
||||
// This makes the local SV obselete.
|
||||
ASSERT_OK(Flush());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_OK(iter->Refresh());
|
||||
iter.reset();
|
||||
// iter used to not cleanup SV, so the Close() below would hit an assertion
|
||||
// error.
|
||||
Close();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+57
-1
@@ -2303,7 +2303,6 @@ TEST_F(DBRangeDelTest, TombstoneOnlyLevel) {
|
||||
InternalIterator* level_iter = sv->current->TEST_GetLevelIterator(
|
||||
read_options, &merge_iter_builder, 1 /* level */, true);
|
||||
// This is needed to make LevelIterator range tombstone aware
|
||||
merge_iter_builder.AddIterator(level_iter);
|
||||
auto miter = merge_iter_builder.Finish();
|
||||
auto k = Key(3);
|
||||
IterKey target;
|
||||
@@ -2688,6 +2687,63 @@ TEST_F(DBRangeDelTest, PrefixSentinelKey) {
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, RefreshMemtableIter) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "a", "z"));
|
||||
ReadOptions ro;
|
||||
ro.read_tier = kMemtableTier;
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(ro)};
|
||||
ASSERT_OK(Flush());
|
||||
// First refresh reinits iter, which had a bug where
|
||||
// iter.memtable_range_tombstone_iter_ was not set to nullptr, and caused
|
||||
// subsequent refresh to double free.
|
||||
ASSERT_OK(iter->Refresh());
|
||||
ASSERT_OK(iter->Refresh());
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, RangeTombstoneRespectIterateUpperBound) {
|
||||
// Memtable: a, [b, bz)
|
||||
// Do a Seek on `a` with iterate_upper_bound being az
|
||||
// range tombstone [b, bz) should not be processed (added to and
|
||||
// popped from the min_heap in MergingIterator).
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "bar"));
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "b", "bz"));
|
||||
|
||||
// I could not find a cleaner way to test this without relying on
|
||||
// implementation detail. Tried to test the value of
|
||||
// `internal_range_del_reseek_count` but that did not work
|
||||
// since BlockBasedTable iterator becomes !Valid() when point key
|
||||
// is out of bound and that reseek only happens when a point key
|
||||
// is covered by some range tombstone.
|
||||
SyncPoint::GetInstance()->SetCallBack("MergeIterator::PopDeleteRangeStart",
|
||||
[](void*) {
|
||||
// there should not be any range
|
||||
// tombstone in the heap.
|
||||
FAIL();
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ReadOptions read_opts;
|
||||
std::string upper_bound = "az";
|
||||
Slice upper_bound_slice = upper_bound;
|
||||
read_opts.iterate_upper_bound = &upper_bound_slice;
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(read_opts)};
|
||||
iter->Seek("a");
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), "a");
|
||||
iter->Next();
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
+18
-9
@@ -958,7 +958,24 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
return true; // to continue to the next seq
|
||||
}
|
||||
|
||||
s->seq = seq;
|
||||
if (s->seq == kMaxSequenceNumber) {
|
||||
s->seq = seq;
|
||||
}
|
||||
|
||||
s->seq = std::max(s->seq, max_covering_tombstone_seq);
|
||||
|
||||
if (ts_sz > 0 && s->timestamp != nullptr) {
|
||||
if (!s->timestamp->empty()) {
|
||||
assert(ts_sz == s->timestamp->size());
|
||||
}
|
||||
// TODO optimize for smaller size ts
|
||||
const std::string kMaxTs(ts_sz, '\xff');
|
||||
if (s->timestamp->empty() ||
|
||||
user_comparator->CompareTimestamp(*(s->timestamp), kMaxTs) == 0) {
|
||||
Slice ts = ExtractTimestampFromUserKey(user_key_slice, ts_sz);
|
||||
s->timestamp->assign(ts.data(), ts_sz);
|
||||
}
|
||||
}
|
||||
|
||||
if ((type == kTypeValue || type == kTypeMerge || type == kTypeBlobIndex ||
|
||||
type == kTypeWideColumnEntity) &&
|
||||
@@ -1039,10 +1056,6 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
*(s->is_blob_index) = (type == kTypeBlobIndex);
|
||||
}
|
||||
|
||||
if (ts_sz > 0 && s->timestamp != nullptr) {
|
||||
Slice ts = ExtractTimestampFromUserKey(user_key_slice, ts_sz);
|
||||
s->timestamp->assign(ts.data(), ts.size());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case kTypeDeletion:
|
||||
@@ -1058,10 +1071,6 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
}
|
||||
} else {
|
||||
*(s->status) = Status::NotFound();
|
||||
if (ts_sz > 0 && s->timestamp != nullptr) {
|
||||
Slice ts = ExtractTimestampFromUserKey(user_key_slice, ts_sz);
|
||||
s->timestamp->assign(ts.data(), ts.size());
|
||||
}
|
||||
}
|
||||
*(s->found_final_value) = true;
|
||||
return false;
|
||||
|
||||
+33
-28
@@ -210,30 +210,6 @@ Status MemTableListVersion::AddRangeTombstoneIterators(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status MemTableListVersion::AddRangeTombstoneIterators(
|
||||
const ReadOptions& read_opts, Arena* /*arena*/,
|
||||
MergeIteratorBuilder& builder) {
|
||||
// Except for snapshot read, using kMaxSequenceNumber is OK because these
|
||||
// are immutable memtables.
|
||||
SequenceNumber read_seq = read_opts.snapshot != nullptr
|
||||
? read_opts.snapshot->GetSequenceNumber()
|
||||
: kMaxSequenceNumber;
|
||||
for (auto& m : memlist_) {
|
||||
auto range_del_iter = m->NewRangeTombstoneIterator(
|
||||
read_opts, read_seq, true /* immutale_memtable */);
|
||||
if (range_del_iter == nullptr || range_del_iter->empty()) {
|
||||
delete range_del_iter;
|
||||
builder.AddRangeTombstoneIterator(nullptr);
|
||||
} else {
|
||||
builder.AddRangeTombstoneIterator(new TruncatedRangeDelIterator(
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator>(range_del_iter),
|
||||
&m->GetInternalKeyComparator(), nullptr /* smallest */,
|
||||
nullptr /* largest */));
|
||||
}
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void MemTableListVersion::AddIterators(
|
||||
const ReadOptions& options, std::vector<InternalIterator*>* iterator_list,
|
||||
Arena* arena) {
|
||||
@@ -242,11 +218,33 @@ void MemTableListVersion::AddIterators(
|
||||
}
|
||||
}
|
||||
|
||||
void MemTableListVersion::AddIterators(
|
||||
const ReadOptions& options, MergeIteratorBuilder* merge_iter_builder) {
|
||||
void MemTableListVersion::AddIterators(const ReadOptions& options,
|
||||
MergeIteratorBuilder* merge_iter_builder,
|
||||
bool add_range_tombstone_iter) {
|
||||
for (auto& m : memlist_) {
|
||||
merge_iter_builder->AddIterator(
|
||||
m->NewIterator(options, merge_iter_builder->GetArena()));
|
||||
auto mem_iter = m->NewIterator(options, merge_iter_builder->GetArena());
|
||||
if (!add_range_tombstone_iter || options.ignore_range_deletions) {
|
||||
merge_iter_builder->AddIterator(mem_iter);
|
||||
} else {
|
||||
// Except for snapshot read, using kMaxSequenceNumber is OK because these
|
||||
// are immutable memtables.
|
||||
SequenceNumber read_seq = options.snapshot != nullptr
|
||||
? options.snapshot->GetSequenceNumber()
|
||||
: kMaxSequenceNumber;
|
||||
TruncatedRangeDelIterator* mem_tombstone_iter = nullptr;
|
||||
auto range_del_iter = m->NewRangeTombstoneIterator(
|
||||
options, read_seq, true /* immutale_memtable */);
|
||||
if (range_del_iter == nullptr || range_del_iter->empty()) {
|
||||
delete range_del_iter;
|
||||
} else {
|
||||
mem_tombstone_iter = new TruncatedRangeDelIterator(
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator>(range_del_iter),
|
||||
&m->GetInternalKeyComparator(), nullptr /* smallest */,
|
||||
nullptr /* largest */);
|
||||
}
|
||||
merge_iter_builder->AddPointAndTombstoneIterator(mem_iter,
|
||||
mem_tombstone_iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +419,13 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
|
||||
std::max(m->GetNextLogNumber(), *max_next_log_number);
|
||||
}
|
||||
ret->push_back(m);
|
||||
} else if (!ret->empty()) {
|
||||
// This `break` is necessary to prevent picking non-consecutive memtables
|
||||
// in case `memlist` has one or more entries with
|
||||
// `flush_in_progress_ == true` sandwiched between entries with
|
||||
// `flush_in_progress_ == false`. This could happen after parallel flushes
|
||||
// are picked and the one flushing older memtables is rolled back.
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!atomic_flush || num_flush_not_started_ == 0) {
|
||||
|
||||
+2
-4
@@ -111,15 +111,13 @@ class MemTableListVersion {
|
||||
Status AddRangeTombstoneIterators(const ReadOptions& read_opts, Arena* arena,
|
||||
RangeDelAggregator* range_del_agg);
|
||||
|
||||
Status AddRangeTombstoneIterators(const ReadOptions& read_opts, Arena* arena,
|
||||
MergeIteratorBuilder& builder);
|
||||
|
||||
void AddIterators(const ReadOptions& options,
|
||||
std::vector<InternalIterator*>* iterator_list,
|
||||
Arena* arena);
|
||||
|
||||
void AddIterators(const ReadOptions& options,
|
||||
MergeIteratorBuilder* merge_iter_builder);
|
||||
MergeIteratorBuilder* merge_iter_builder,
|
||||
bool add_range_tombstone_iter);
|
||||
|
||||
uint64_t GetTotalNumEntries() const;
|
||||
|
||||
|
||||
+35
-13
@@ -742,28 +742,40 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
// Pick tables to flush
|
||||
list.PickMemtablesToFlush(
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, &to_flush);
|
||||
// Should pick 4 of 5 since 1 table has been picked in to_flush2
|
||||
ASSERT_EQ(4, to_flush.size());
|
||||
// Picks three oldest memtables. The fourth oldest is picked in `to_flush2` so
|
||||
// must be excluded. The newest (fifth oldest) is non-consecutive with the
|
||||
// three oldest due to omitting the fourth oldest so must not be picked.
|
||||
ASSERT_EQ(3, to_flush.size());
|
||||
ASSERT_EQ(5, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_TRUE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Pick tables to flush again
|
||||
autovector<MemTable*> to_flush3;
|
||||
list.PickMemtablesToFlush(
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, &to_flush3);
|
||||
ASSERT_EQ(0, to_flush3.size()); // nothing not in progress of being flushed
|
||||
// Picks newest (fifth oldest)
|
||||
ASSERT_EQ(1, to_flush3.size());
|
||||
ASSERT_EQ(5, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Flush the 4 memtables that were picked in to_flush
|
||||
// Nothing left to flush
|
||||
autovector<MemTable*> to_flush4;
|
||||
list.PickMemtablesToFlush(
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, &to_flush4);
|
||||
ASSERT_EQ(0, to_flush4.size());
|
||||
ASSERT_EQ(5, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Flush the 3 memtables that were picked in to_flush
|
||||
s = Mock_InstallMemtableFlushResults(&list, mutable_cf_options, to_flush,
|
||||
&to_delete);
|
||||
ASSERT_OK(s);
|
||||
|
||||
// Note: now to_flush contains tables[0,1,2,4]. to_flush2 contains
|
||||
// tables[3].
|
||||
// Note: now to_flush contains tables[0,1,2]. to_flush2 contains
|
||||
// tables[3]. to_flush3 contains tables[4].
|
||||
// Current implementation will only commit memtables in the order they were
|
||||
// created. So TryInstallMemtableFlushResults will install the first 3 tables
|
||||
// in to_flush and stop when it encounters a table not yet flushed.
|
||||
@@ -779,7 +791,17 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Flush the 1 memtable that was picked in to_flush2
|
||||
// Flush the 1 memtable (tables[4]) that was picked in to_flush3
|
||||
s = MemTableListTest::Mock_InstallMemtableFlushResults(
|
||||
&list, mutable_cf_options, to_flush3, &to_delete);
|
||||
ASSERT_OK(s);
|
||||
|
||||
// This will install 0 tables since tables[4] flushed while tables[3] has not
|
||||
// yet flushed.
|
||||
ASSERT_EQ(2, list.NumNotFlushed());
|
||||
ASSERT_EQ(0, to_delete.size());
|
||||
|
||||
// Flush the 1 memtable (tables[3]) that was picked in to_flush2
|
||||
s = MemTableListTest::Mock_InstallMemtableFlushResults(
|
||||
&list, mutable_cf_options, to_flush2, &to_delete);
|
||||
ASSERT_OK(s);
|
||||
@@ -809,11 +831,11 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
memtable_id = 4;
|
||||
// Pick tables to flush. The tables to pick must have ID smaller than or
|
||||
// equal to 4. Therefore, no table will be selected in this case.
|
||||
autovector<MemTable*> to_flush4;
|
||||
autovector<MemTable*> to_flush5;
|
||||
list.FlushRequested();
|
||||
ASSERT_TRUE(list.HasFlushRequested());
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush4);
|
||||
ASSERT_TRUE(to_flush4.empty());
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush5);
|
||||
ASSERT_TRUE(to_flush5.empty());
|
||||
ASSERT_EQ(1, list.NumNotFlushed());
|
||||
ASSERT_TRUE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
@@ -823,8 +845,8 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
// equal to 5. Therefore, only tables[5] will be selected.
|
||||
memtable_id = 5;
|
||||
list.FlushRequested();
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush4);
|
||||
ASSERT_EQ(1, static_cast<int>(to_flush4.size()));
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush5);
|
||||
ASSERT_EQ(1, static_cast<int>(to_flush5.size()));
|
||||
ASSERT_EQ(1, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
|
||||
+48
-27
@@ -941,18 +941,18 @@ namespace {
|
||||
class LevelIterator final : public InternalIterator {
|
||||
public:
|
||||
// @param read_options Must outlive this iterator.
|
||||
LevelIterator(TableCache* table_cache, const ReadOptions& read_options,
|
||||
const FileOptions& file_options,
|
||||
const InternalKeyComparator& icomparator,
|
||||
const LevelFilesBrief* flevel,
|
||||
const std::shared_ptr<const SliceTransform>& prefix_extractor,
|
||||
bool should_sample, HistogramImpl* file_read_hist,
|
||||
TableReaderCaller caller, bool skip_filters, int level,
|
||||
RangeDelAggregator* range_del_agg,
|
||||
const std::vector<AtomicCompactionUnitBoundary>*
|
||||
compaction_boundaries = nullptr,
|
||||
bool allow_unprepared_value = false,
|
||||
MergeIteratorBuilder* merge_iter_builder = nullptr)
|
||||
LevelIterator(
|
||||
TableCache* table_cache, const ReadOptions& read_options,
|
||||
const FileOptions& file_options, const InternalKeyComparator& icomparator,
|
||||
const LevelFilesBrief* flevel,
|
||||
const std::shared_ptr<const SliceTransform>& prefix_extractor,
|
||||
bool should_sample, HistogramImpl* file_read_hist,
|
||||
TableReaderCaller caller, bool skip_filters, int level,
|
||||
RangeDelAggregator* range_del_agg,
|
||||
const std::vector<AtomicCompactionUnitBoundary>* compaction_boundaries =
|
||||
nullptr,
|
||||
bool allow_unprepared_value = false,
|
||||
TruncatedRangeDelIterator**** range_tombstone_iter_ptr_ = nullptr)
|
||||
: table_cache_(table_cache),
|
||||
read_options_(read_options),
|
||||
file_options_(file_options),
|
||||
@@ -975,10 +975,8 @@ class LevelIterator final : public InternalIterator {
|
||||
to_return_sentinel_(false) {
|
||||
// Empty level is not supported.
|
||||
assert(flevel_ != nullptr && flevel_->num_files > 0);
|
||||
if (merge_iter_builder && !read_options.ignore_range_deletions) {
|
||||
// lazily initialize range_tombstone_iter_ together with file_iter_
|
||||
merge_iter_builder->AddRangeTombstoneIterator(nullptr,
|
||||
&range_tombstone_iter_);
|
||||
if (range_tombstone_iter_ptr_) {
|
||||
*range_tombstone_iter_ptr_ = &range_tombstone_iter_;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1840,14 +1838,22 @@ InternalIterator* Version::TEST_GetLevelIterator(
|
||||
int level, bool allow_unprepared_value) {
|
||||
auto* arena = merge_iter_builder->GetArena();
|
||||
auto* mem = arena->AllocateAligned(sizeof(LevelIterator));
|
||||
return new (mem) LevelIterator(
|
||||
TruncatedRangeDelIterator*** tombstone_iter_ptr = nullptr;
|
||||
auto level_iter = new (mem) LevelIterator(
|
||||
cfd_->table_cache(), read_options, file_options_,
|
||||
cfd_->internal_comparator(), &storage_info_.LevelFilesBrief(level),
|
||||
mutable_cf_options_.prefix_extractor, should_sample_file_read(),
|
||||
cfd_->internal_stats()->GetFileReadHist(level),
|
||||
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
|
||||
nullptr /* range_del_agg */, nullptr /* compaction_boundaries */,
|
||||
allow_unprepared_value, merge_iter_builder);
|
||||
allow_unprepared_value, &tombstone_iter_ptr);
|
||||
if (read_options.ignore_range_deletions) {
|
||||
merge_iter_builder->AddIterator(level_iter);
|
||||
} else {
|
||||
merge_iter_builder->AddPointAndTombstoneIterator(
|
||||
level_iter, nullptr /* tombstone_iter */, tombstone_iter_ptr);
|
||||
}
|
||||
return level_iter;
|
||||
}
|
||||
|
||||
uint64_t VersionStorageInfo::GetEstimatedActiveKeys() const {
|
||||
@@ -1927,10 +1933,10 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
|
||||
auto* arena = merge_iter_builder->GetArena();
|
||||
if (level == 0) {
|
||||
// Merge all level zero files together since they may overlap
|
||||
TruncatedRangeDelIterator* iter = nullptr;
|
||||
TruncatedRangeDelIterator* tombstone_iter = nullptr;
|
||||
for (size_t i = 0; i < storage_info_.LevelFilesBrief(0).num_files; i++) {
|
||||
const auto& file = storage_info_.LevelFilesBrief(0).files[i];
|
||||
merge_iter_builder->AddIterator(cfd_->table_cache()->NewIterator(
|
||||
auto table_iter = cfd_->table_cache()->NewIterator(
|
||||
read_options, soptions, cfd_->internal_comparator(),
|
||||
*file.file_metadata, /*range_del_agg=*/nullptr,
|
||||
mutable_cf_options_.prefix_extractor, nullptr,
|
||||
@@ -1938,9 +1944,13 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
|
||||
TableReaderCaller::kUserIterator, arena,
|
||||
/*skip_filters=*/false, /*level=*/0, max_file_size_for_l0_meta_pin_,
|
||||
/*smallest_compaction_key=*/nullptr,
|
||||
/*largest_compaction_key=*/nullptr, allow_unprepared_value, &iter));
|
||||
if (!read_options.ignore_range_deletions) {
|
||||
merge_iter_builder->AddRangeTombstoneIterator(iter);
|
||||
/*largest_compaction_key=*/nullptr, allow_unprepared_value,
|
||||
&tombstone_iter);
|
||||
if (read_options.ignore_range_deletions) {
|
||||
merge_iter_builder->AddIterator(table_iter);
|
||||
} else {
|
||||
merge_iter_builder->AddPointAndTombstoneIterator(table_iter,
|
||||
tombstone_iter);
|
||||
}
|
||||
}
|
||||
if (should_sample) {
|
||||
@@ -1957,14 +1967,21 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
|
||||
// walks through the non-overlapping files in the level, opening them
|
||||
// lazily.
|
||||
auto* mem = arena->AllocateAligned(sizeof(LevelIterator));
|
||||
merge_iter_builder->AddIterator(new (mem) LevelIterator(
|
||||
TruncatedRangeDelIterator*** tombstone_iter_ptr = nullptr;
|
||||
auto level_iter = new (mem) LevelIterator(
|
||||
cfd_->table_cache(), read_options, soptions,
|
||||
cfd_->internal_comparator(), &storage_info_.LevelFilesBrief(level),
|
||||
mutable_cf_options_.prefix_extractor, should_sample_file_read(),
|
||||
cfd_->internal_stats()->GetFileReadHist(level),
|
||||
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
|
||||
/*range_del_agg=*/nullptr, /*compaction_boundaries=*/nullptr,
|
||||
allow_unprepared_value, merge_iter_builder));
|
||||
allow_unprepared_value, &tombstone_iter_ptr);
|
||||
if (read_options.ignore_range_deletions) {
|
||||
merge_iter_builder->AddIterator(level_iter);
|
||||
} else {
|
||||
merge_iter_builder->AddPointAndTombstoneIterator(
|
||||
level_iter, nullptr /* tombstone_iter */, tombstone_iter_ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2897,7 +2914,9 @@ void VersionStorageInfo::PrepareForVersionAppend(
|
||||
GenerateFileIndexer();
|
||||
GenerateLevelFilesBrief();
|
||||
GenerateLevel0NonOverlapping();
|
||||
GenerateBottommostFiles();
|
||||
if (!immutable_options.allow_ingest_behind) {
|
||||
GenerateBottommostFiles();
|
||||
}
|
||||
GenerateFileLocationIndex();
|
||||
}
|
||||
|
||||
@@ -3337,7 +3356,9 @@ void VersionStorageInfo::ComputeCompactionScore(
|
||||
}
|
||||
}
|
||||
ComputeFilesMarkedForCompaction();
|
||||
ComputeBottommostFilesMarkedForCompaction();
|
||||
if (!immutable_options.allow_ingest_behind) {
|
||||
ComputeBottommostFilesMarkedForCompaction();
|
||||
}
|
||||
if (mutable_cf_options.ttl > 0) {
|
||||
ComputeExpiredTtlFiles(immutable_options, mutable_cf_options.ttl);
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@ DECLARE_bool(mock_direct_io);
|
||||
DECLARE_bool(statistics);
|
||||
DECLARE_bool(sync);
|
||||
DECLARE_bool(use_fsync);
|
||||
DECLARE_uint64(stats_dump_period_sec);
|
||||
DECLARE_uint64(bytes_per_sync);
|
||||
DECLARE_uint64(wal_bytes_per_sync);
|
||||
DECLARE_int32(kill_random_test);
|
||||
|
||||
@@ -1052,4 +1052,8 @@ DEFINE_bool(
|
||||
"be preserved similarly under `FLAGS_expected_values_dir/unverified` when "
|
||||
"`--expected_values_dir` is nonempty.");
|
||||
|
||||
DEFINE_uint64(stats_dump_period_sec,
|
||||
ROCKSDB_NAMESPACE::Options().stats_dump_period_sec,
|
||||
"Gap between printing stats to log in seconds");
|
||||
|
||||
#endif // GFLAGS
|
||||
|
||||
@@ -3051,6 +3051,8 @@ void InitializeOptionsFromFlags(
|
||||
options.experimental_mempurge_threshold =
|
||||
FLAGS_experimental_mempurge_threshold;
|
||||
options.periodic_compaction_seconds = FLAGS_periodic_compaction_seconds;
|
||||
options.stats_dump_period_sec =
|
||||
static_cast<unsigned int>(FLAGS_stats_dump_period_sec);
|
||||
options.ttl = FLAGS_compaction_ttl;
|
||||
options.enable_pipelined_write = FLAGS_enable_pipelined_write;
|
||||
options.enable_write_thread_adaptive_yield =
|
||||
|
||||
Vendored
+6
-4
@@ -1115,10 +1115,11 @@ class PosixFileSystem : public FileSystem {
|
||||
// Prepare the cancel request.
|
||||
struct io_uring_sqe* sqe;
|
||||
sqe = io_uring_get_sqe(iu);
|
||||
// prep_cancel changed API in liburing, but we need to support both old
|
||||
// and new versions so do it by hand
|
||||
io_uring_prep_cancel(sqe, 0, 0);
|
||||
sqe->addr = reinterpret_cast<uint64_t>(posix_handle);
|
||||
|
||||
// In order to cancel the request, sqe->addr of cancel request should
|
||||
// match with the read request submitted which is posix_handle->iov.
|
||||
io_uring_prep_cancel(sqe, &posix_handle->iov, 0);
|
||||
// Sets sqe->user_data to posix_handle.
|
||||
io_uring_sqe_set_data(sqe, posix_handle);
|
||||
|
||||
// submit the request.
|
||||
@@ -1146,6 +1147,7 @@ class PosixFileSystem : public FileSystem {
|
||||
}
|
||||
assert(cqe != nullptr);
|
||||
|
||||
// Returns cqe->user_data.
|
||||
Posix_IOHandle* posix_handle =
|
||||
static_cast<Posix_IOHandle*>(io_uring_cqe_get_data(cqe));
|
||||
assert(posix_handle->iu == iu);
|
||||
|
||||
Vendored
+3
-1
@@ -899,8 +899,10 @@ IOStatus PosixRandomAccessFile::ReadAsync(
|
||||
struct io_uring_sqe* sqe;
|
||||
sqe = io_uring_get_sqe(iu);
|
||||
|
||||
io_uring_prep_readv(sqe, fd_, &posix_handle->iov, 1, posix_handle->offset);
|
||||
io_uring_prep_readv(sqe, fd_, /*sqe->addr=*/&posix_handle->iov,
|
||||
/*sqe->len=*/1, /*sqe->offset=*/posix_handle->offset);
|
||||
|
||||
// Sets sqe->user_data to posix_handle.
|
||||
io_uring_sqe_set_data(sqe, posix_handle);
|
||||
|
||||
// Step 4: io_uring_submit
|
||||
|
||||
+134
-65
@@ -225,10 +225,8 @@ void FilePrefetchBuffer::AbortIOIfNeeded(uint64_t offset) {
|
||||
bufs_[second].async_read_in_progress_ = false;
|
||||
}
|
||||
|
||||
if (bufs_[curr_].io_handle_ == nullptr &&
|
||||
bufs_[curr_].async_read_in_progress_) {
|
||||
if (bufs_[curr_].io_handle_ == nullptr) {
|
||||
bufs_[curr_].async_read_in_progress_ = false;
|
||||
curr_ = curr_ ^ 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,16 +266,36 @@ void FilePrefetchBuffer::UpdateBuffersIfNeeded(uint64_t offset) {
|
||||
bufs_[second].buffer_.Clear();
|
||||
}
|
||||
|
||||
// If data starts from second buffer, make it curr_. Second buffer can be
|
||||
// either partial filled or full.
|
||||
if (!bufs_[second].async_read_in_progress_ && DoesBufferContainData(second) &&
|
||||
IsOffsetInBuffer(offset, second)) {
|
||||
// Clear the curr_ as buffers have been swapped and curr_ contains the
|
||||
// outdated data and switch the buffers.
|
||||
if (!bufs_[curr_].async_read_in_progress_) {
|
||||
bufs_[curr_].buffer_.Clear();
|
||||
{
|
||||
// In case buffers do not align, reset second buffer. This can happen in
|
||||
// case readahead_size is set.
|
||||
if (!bufs_[second].async_read_in_progress_ &&
|
||||
!bufs_[curr_].async_read_in_progress_) {
|
||||
if (DoesBufferContainData(curr_)) {
|
||||
if (bufs_[curr_].offset_ + bufs_[curr_].buffer_.CurrentSize() !=
|
||||
bufs_[second].offset_) {
|
||||
bufs_[second].buffer_.Clear();
|
||||
}
|
||||
} else {
|
||||
if (!IsOffsetInBuffer(offset, second)) {
|
||||
bufs_[second].buffer_.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If data starts from second buffer, make it curr_. Second buffer can be
|
||||
// either partial filled, full or async read is in progress.
|
||||
if (bufs_[second].async_read_in_progress_) {
|
||||
if (IsOffsetInBufferWithAsyncProgress(offset, second)) {
|
||||
curr_ = curr_ ^ 1;
|
||||
}
|
||||
} else {
|
||||
if (DoesBufferContainData(second) && IsOffsetInBuffer(offset, second)) {
|
||||
assert(bufs_[curr_].async_read_in_progress_ ||
|
||||
bufs_[curr_].buffer_.CurrentSize() == 0);
|
||||
curr_ = curr_ ^ 1;
|
||||
}
|
||||
curr_ = curr_ ^ 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,53 +318,16 @@ void FilePrefetchBuffer::PollAndUpdateBuffersIfNeeded(uint64_t offset) {
|
||||
UpdateBuffersIfNeeded(offset);
|
||||
}
|
||||
|
||||
// If async_io is enabled in case of sequential reads, PrefetchAsyncInternal is
|
||||
// called. When buffers are switched, we clear the curr_ buffer as we assume the
|
||||
// data has been consumed because of sequential reads.
|
||||
// Data in buffers will always be sequential with curr_ following second and
|
||||
// not vice versa.
|
||||
//
|
||||
// Scenarios for prefetching asynchronously:
|
||||
// Case1: If both buffers are empty, prefetch n + readahead_size_/2 bytes
|
||||
// synchronously in curr_ and prefetch readahead_size_/2 async in second
|
||||
// buffer.
|
||||
// Case2: If second buffer has partial or full data, make it current and
|
||||
// prefetch readahead_size_/2 async in second buffer. In case of
|
||||
// partial data, prefetch remaining bytes from size n synchronously to
|
||||
// fulfill the requested bytes request.
|
||||
// Case3: If curr_ has partial data, prefetch remaining bytes from size n
|
||||
// synchronously in curr_ to fulfill the requested bytes request and
|
||||
// prefetch readahead_size_/2 bytes async in second buffer.
|
||||
// Case4: (Special case) If data is in both buffers, copy requested data from
|
||||
// curr_, send async request on curr_, wait for poll to fill second
|
||||
// buffer (if any), and copy remaining data from second buffer to third
|
||||
// buffer.
|
||||
Status FilePrefetchBuffer::PrefetchAsyncInternal(
|
||||
Status FilePrefetchBuffer::HandleOverlappingData(
|
||||
const IOOptions& opts, RandomAccessFileReader* reader, uint64_t offset,
|
||||
size_t length, size_t readahead_size, Env::IOPriority rate_limiter_priority,
|
||||
bool& copy_to_third_buffer) {
|
||||
if (!enable_) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("FilePrefetchBuffer::PrefetchAsyncInternal:Start");
|
||||
|
||||
size_t alignment = reader->file()->GetRequiredBufferAlignment();
|
||||
size_t length, size_t readahead_size,
|
||||
Env::IOPriority /*rate_limiter_priority*/, bool& copy_to_third_buffer,
|
||||
uint64_t& tmp_offset, size_t& tmp_length) {
|
||||
Status s;
|
||||
uint64_t tmp_offset = offset;
|
||||
size_t tmp_length = length;
|
||||
|
||||
// 1. Abort IO and swap buffers if needed to point curr_ to first buffer with
|
||||
// data.
|
||||
{
|
||||
if (!explicit_prefetch_submitted_) {
|
||||
AbortIOIfNeeded(offset);
|
||||
}
|
||||
UpdateBuffersIfNeeded(offset);
|
||||
}
|
||||
size_t alignment = reader->file()->GetRequiredBufferAlignment();
|
||||
uint32_t second = curr_ ^ 1;
|
||||
|
||||
// 2. If data is overlapping over two buffers, copy the data from curr_ and
|
||||
// If data is overlapping over two buffers, copy the data from curr_ and
|
||||
// call ReadAsync on curr_.
|
||||
if (!bufs_[curr_].async_read_in_progress_ && DoesBufferContainData(curr_) &&
|
||||
IsOffsetInBuffer(offset, curr_) &&
|
||||
@@ -391,21 +372,80 @@ Status FilePrefetchBuffer::PrefetchAsyncInternal(
|
||||
}
|
||||
curr_ = curr_ ^ 1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
// If async_io is enabled in case of sequential reads, PrefetchAsyncInternal is
|
||||
// called. When buffers are switched, we clear the curr_ buffer as we assume the
|
||||
// data has been consumed because of sequential reads.
|
||||
// Data in buffers will always be sequential with curr_ following second and
|
||||
// not vice versa.
|
||||
//
|
||||
// Scenarios for prefetching asynchronously:
|
||||
// Case1: If both buffers are empty, prefetch n + readahead_size_/2 bytes
|
||||
// synchronously in curr_ and prefetch readahead_size_/2 async in second
|
||||
// buffer.
|
||||
// Case2: If second buffer has partial or full data, make it current and
|
||||
// prefetch readahead_size_/2 async in second buffer. In case of
|
||||
// partial data, prefetch remaining bytes from size n synchronously to
|
||||
// fulfill the requested bytes request.
|
||||
// Case3: If curr_ has partial data, prefetch remaining bytes from size n
|
||||
// synchronously in curr_ to fulfill the requested bytes request and
|
||||
// prefetch readahead_size_/2 bytes async in second buffer.
|
||||
// Case4: (Special case) If data is in both buffers, copy requested data from
|
||||
// curr_, send async request on curr_, wait for poll to fill second
|
||||
// buffer (if any), and copy remaining data from second buffer to third
|
||||
// buffer.
|
||||
Status FilePrefetchBuffer::PrefetchAsyncInternal(
|
||||
const IOOptions& opts, RandomAccessFileReader* reader, uint64_t offset,
|
||||
size_t length, size_t readahead_size, Env::IOPriority rate_limiter_priority,
|
||||
bool& copy_to_third_buffer) {
|
||||
if (!enable_) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("FilePrefetchBuffer::PrefetchAsyncInternal:Start");
|
||||
|
||||
size_t alignment = reader->file()->GetRequiredBufferAlignment();
|
||||
Status s;
|
||||
uint64_t tmp_offset = offset;
|
||||
size_t tmp_length = length;
|
||||
|
||||
// 1. Abort IO and swap buffers if needed to point curr_ to first buffer with
|
||||
// data.
|
||||
if (!explicit_prefetch_submitted_) {
|
||||
AbortIOIfNeeded(offset);
|
||||
}
|
||||
UpdateBuffersIfNeeded(offset);
|
||||
|
||||
// 2. Handle overlapping data over two buffers. If data is overlapping then
|
||||
// during this call:
|
||||
// - data from curr_ is copied into third buffer,
|
||||
// - curr_ is send for async prefetching of further data if second buffer
|
||||
// contains remaining requested data or in progress for async prefetch,
|
||||
// - switch buffers and curr_ now points to second buffer to copy remaining
|
||||
// data.
|
||||
s = HandleOverlappingData(opts, reader, offset, length, readahead_size,
|
||||
rate_limiter_priority, copy_to_third_buffer,
|
||||
tmp_offset, tmp_length);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// 3. Call Poll only if data is needed for the second buffer.
|
||||
// - Return if whole data is in curr_ and second buffer in progress.
|
||||
// - Return if whole data is in curr_ and second buffer is in progress or
|
||||
// already full.
|
||||
// - If second buffer is empty, it will go for ReadAsync for second buffer.
|
||||
if (!bufs_[curr_].async_read_in_progress_ && DoesBufferContainData(curr_) &&
|
||||
IsDataBlockInBuffer(offset, length, curr_)) {
|
||||
// Whole data is in curr_.
|
||||
UpdateBuffersIfNeeded(offset);
|
||||
second = curr_ ^ 1;
|
||||
if (bufs_[second].async_read_in_progress_) {
|
||||
if (!IsSecondBuffEligibleForPrefetching()) {
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
// After poll request, curr_ might be empty because of IOError in
|
||||
// callback while reading or may contain required data.
|
||||
PollAndUpdateBuffersIfNeeded(offset);
|
||||
second = curr_ ^ 1;
|
||||
}
|
||||
|
||||
if (copy_to_third_buffer) {
|
||||
@@ -427,19 +467,42 @@ Status FilePrefetchBuffer::PrefetchAsyncInternal(
|
||||
if (explicit_prefetch_submitted_) {
|
||||
return s;
|
||||
}
|
||||
if (!IsSecondBuffEligibleForPrefetching()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t second = curr_ ^ 1;
|
||||
assert(!bufs_[curr_].async_read_in_progress_);
|
||||
|
||||
// In case because of some IOError curr_ got empty, abort IO for second as
|
||||
// well. Otherwise data might not align if more data needs to be read in curr_
|
||||
// which might overlap with second buffer.
|
||||
if (!DoesBufferContainData(curr_) && bufs_[second].async_read_in_progress_) {
|
||||
if (bufs_[second].io_handle_ != nullptr) {
|
||||
std::vector<void*> handles;
|
||||
handles.emplace_back(bufs_[second].io_handle_);
|
||||
{
|
||||
StopWatch sw(clock_, stats_, ASYNC_PREFETCH_ABORT_MICROS);
|
||||
Status status = fs_->AbortIO(handles);
|
||||
assert(status.ok());
|
||||
}
|
||||
}
|
||||
DestroyAndClearIOHandle(second);
|
||||
bufs_[second].buffer_.Clear();
|
||||
}
|
||||
|
||||
// 5. Data is overlapping i.e. some of the data has been copied to third
|
||||
// buffer
|
||||
// and remaining will be updated below.
|
||||
if (copy_to_third_buffer) {
|
||||
// buffer and remaining will be updated below.
|
||||
if (copy_to_third_buffer && DoesBufferContainData(curr_)) {
|
||||
CopyDataToBuffer(curr_, offset, length);
|
||||
|
||||
// Length == 0: All the requested data has been copied to third buffer and
|
||||
// it has already gone for async prefetching. It can return without doing
|
||||
// anything further.
|
||||
// Length > 0: More data needs to be consumed so it will continue async and
|
||||
// sync prefetching and copy the remaining data to third buffer in the end.
|
||||
// Length > 0: More data needs to be consumed so it will continue async
|
||||
// and sync prefetching and copy the remaining data to third buffer in the
|
||||
// end.
|
||||
if (length == 0) {
|
||||
return s;
|
||||
}
|
||||
@@ -458,6 +521,9 @@ Status FilePrefetchBuffer::PrefetchAsyncInternal(
|
||||
uint64_t chunk_len1 = 0;
|
||||
uint64_t read_len1 = 0;
|
||||
|
||||
assert(!bufs_[second].async_read_in_progress_ &&
|
||||
!DoesBufferContainData(second));
|
||||
|
||||
// For length == 0, skip the synchronous prefetching. read_len1 will be 0.
|
||||
if (length > 0) {
|
||||
CalculateOffsetAndLen(alignment, offset, roundup_len1, curr_,
|
||||
@@ -594,8 +660,11 @@ bool FilePrefetchBuffer::TryReadFromCacheAsync(
|
||||
}
|
||||
|
||||
if (explicit_prefetch_submitted_) {
|
||||
// explicit_prefetch_submitted_ is special case where it expects request
|
||||
// submitted in PrefetchAsync should match with this request. Otherwise
|
||||
// buffers will be outdated.
|
||||
// Random offset called. So abort the IOs.
|
||||
if (prev_offset_ != offset) {
|
||||
// Random offset called. So abort the IOs.
|
||||
AbortAllIOs();
|
||||
bufs_[curr_].buffer_.Clear();
|
||||
bufs_[curr_ ^ 1].buffer_.Clear();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <sstream>
|
||||
@@ -236,6 +237,7 @@ class FilePrefetchBuffer {
|
||||
}
|
||||
prev_offset_ = offset;
|
||||
prev_len_ = len;
|
||||
explicit_prefetch_submitted_ = false;
|
||||
}
|
||||
|
||||
void GetReadaheadState(ReadaheadFileInfo::ReadaheadInfo* readahead_info) {
|
||||
@@ -363,6 +365,27 @@ class FilePrefetchBuffer {
|
||||
bufs_[index].io_handle_ != nullptr &&
|
||||
offset >= bufs_[index].offset_ + bufs_[index].async_req_len_);
|
||||
}
|
||||
bool IsOffsetInBufferWithAsyncProgress(uint64_t offset, uint32_t index) {
|
||||
return (bufs_[index].async_read_in_progress_ &&
|
||||
offset >= bufs_[index].offset_ &&
|
||||
offset < bufs_[index].offset_ + bufs_[index].async_req_len_);
|
||||
}
|
||||
|
||||
bool IsSecondBuffEligibleForPrefetching() {
|
||||
uint32_t second = curr_ ^ 1;
|
||||
if (bufs_[second].async_read_in_progress_) {
|
||||
return false;
|
||||
}
|
||||
assert(!bufs_[curr_].async_read_in_progress_);
|
||||
|
||||
if (DoesBufferContainData(curr_) && DoesBufferContainData(second) &&
|
||||
(bufs_[curr_].offset_ + bufs_[curr_].buffer_.CurrentSize() ==
|
||||
bufs_[second].offset_)) {
|
||||
return false;
|
||||
}
|
||||
bufs_[second].buffer_.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DestroyAndClearIOHandle(uint32_t index) {
|
||||
if (bufs_[index].io_handle_ != nullptr && bufs_[index].del_fn_ != nullptr) {
|
||||
@@ -373,6 +396,13 @@ class FilePrefetchBuffer {
|
||||
bufs_[index].async_read_in_progress_ = false;
|
||||
}
|
||||
|
||||
Status HandleOverlappingData(const IOOptions& opts,
|
||||
RandomAccessFileReader* reader, uint64_t offset,
|
||||
size_t length, size_t readahead_size,
|
||||
Env::IOPriority rate_limiter_priority,
|
||||
bool& copy_to_third_buffer, uint64_t& tmp_offset,
|
||||
size_t& tmp_length);
|
||||
|
||||
std::vector<BufferInfo> bufs_;
|
||||
// curr_ represents the index for bufs_ indicating which buffer is being
|
||||
// consumed currently.
|
||||
|
||||
@@ -471,6 +471,7 @@ IOStatus RandomAccessFileReader::ReadAsync(
|
||||
(uintptr_t(req.scratch) & (alignment - 1)) == 0;
|
||||
read_async_info->is_aligned_ = is_aligned;
|
||||
|
||||
uint64_t elapsed = 0;
|
||||
if (use_direct_io() && is_aligned == false) {
|
||||
FSReadRequest aligned_req = Align(req, alignment);
|
||||
aligned_req.status.PermitUncheckedError();
|
||||
@@ -491,12 +492,17 @@ IOStatus RandomAccessFileReader::ReadAsync(
|
||||
|
||||
assert(read_async_info->buf_.CurrentSize() == 0);
|
||||
|
||||
StopWatch sw(clock_, nullptr /*stats*/, 0 /*hist_type*/, &elapsed,
|
||||
true /*overwrite*/, true /*delay_enabled*/);
|
||||
s = file_->ReadAsync(aligned_req, opts, read_async_callback,
|
||||
read_async_info, io_handle, del_fn, nullptr /*dbg*/);
|
||||
} else {
|
||||
StopWatch sw(clock_, nullptr /*stats*/, 0 /*hist_type*/, &elapsed,
|
||||
true /*overwrite*/, true /*delay_enabled*/);
|
||||
s = file_->ReadAsync(req, opts, read_async_callback, read_async_info,
|
||||
io_handle, del_fn, nullptr /*dbg*/);
|
||||
}
|
||||
RecordTick(stats_, READ_ASYNC_MICROS, elapsed);
|
||||
|
||||
// Suppress false positive clang analyzer warnings.
|
||||
// Memory is not released if file_->ReadAsync returns !s.ok(), because
|
||||
@@ -575,6 +581,8 @@ void RandomAccessFileReader::ReadAsyncCallback(const FSReadRequest& req,
|
||||
}
|
||||
if (req.status.ok()) {
|
||||
RecordInHistogram(stats_, ASYNC_READ_BYTES, req.result.size());
|
||||
} else if (!req.status.IsAborted()) {
|
||||
RecordTick(stats_, ASYNC_READ_ERROR_COUNT, 1);
|
||||
}
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (ShouldNotifyListeners()) {
|
||||
|
||||
@@ -271,7 +271,10 @@ struct AdvancedColumnFamilyOptions {
|
||||
// read amplification because a get request has to check in all of these
|
||||
// files. Also, an in-memory merge may result in writing lesser
|
||||
// data to storage if there are duplicate records in each of these
|
||||
// individual write buffers. Default: 1
|
||||
// individual write buffers.
|
||||
// If atomic flush is enabled (options.atomic_flush == true), then this
|
||||
// option will be sanitized to 1.
|
||||
// Default: 1
|
||||
int min_write_buffer_number_to_merge = 1;
|
||||
|
||||
// DEPRECATED
|
||||
|
||||
@@ -1480,7 +1480,7 @@ struct ReadOptions {
|
||||
const Slice* iterate_lower_bound;
|
||||
|
||||
// "iterate_upper_bound" defines the extent up to which the forward iterator
|
||||
// can returns entries. Once the bound is reached, Valid() will be false.
|
||||
// can return entries. Once the bound is reached, Valid() will be false.
|
||||
// "iterate_upper_bound" is exclusive ie the bound value is
|
||||
// not a valid entry. If prefix_extractor is not null:
|
||||
// 1. If options.auto_prefix_mode = true, iterate_upper_bound will be used
|
||||
|
||||
@@ -448,6 +448,11 @@ enum Tickers : uint32_t {
|
||||
// # of bytes written into blob cache.
|
||||
BLOB_DB_CACHE_BYTES_WRITE,
|
||||
|
||||
// Time spent in the ReadAsync file system call
|
||||
READ_ASYNC_MICROS,
|
||||
// Number of errors returned to the async read callback
|
||||
ASYNC_READ_ERROR_COUNT,
|
||||
|
||||
TICKER_ENUM_MAX
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 7
|
||||
#define ROCKSDB_MINOR 7
|
||||
#define ROCKSDB_PATCH 0
|
||||
#define ROCKSDB_PATCH 8
|
||||
|
||||
// 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
|
||||
|
||||
@@ -5104,6 +5104,10 @@ class TickerTypeJni {
|
||||
return -0x33;
|
||||
case ROCKSDB_NAMESPACE::Tickers::BLOB_DB_CACHE_BYTES_WRITE:
|
||||
return -0x34;
|
||||
case ROCKSDB_NAMESPACE::Tickers::READ_ASYNC_MICROS:
|
||||
return -0x35;
|
||||
case ROCKSDB_NAMESPACE::Tickers::ASYNC_READ_ERROR_COUNT:
|
||||
return -0x36;
|
||||
case ROCKSDB_NAMESPACE::Tickers::TICKER_ENUM_MAX:
|
||||
// 0x5F was the max value in the initial copy of tickers to Java.
|
||||
// Since these values are exposed directly to Java clients, we keep
|
||||
@@ -5487,6 +5491,10 @@ class TickerTypeJni {
|
||||
return ROCKSDB_NAMESPACE::Tickers::BLOB_DB_CACHE_BYTES_READ;
|
||||
case -0x34:
|
||||
return ROCKSDB_NAMESPACE::Tickers::BLOB_DB_CACHE_BYTES_WRITE;
|
||||
case -0x35:
|
||||
return ROCKSDB_NAMESPACE::Tickers::READ_ASYNC_MICROS;
|
||||
case -0x36:
|
||||
return ROCKSDB_NAMESPACE::Tickers::ASYNC_READ_ERROR_COUNT;
|
||||
case 0x5F:
|
||||
// 0x5F was the max value in the initial copy of tickers to Java.
|
||||
// Since these values are exposed directly to Java clients, we keep
|
||||
|
||||
@@ -233,7 +233,9 @@ const std::vector<std::pair<Tickers, std::string>> TickersNameMap = {
|
||||
{BLOB_DB_CACHE_ADD, "rocksdb.blobdb.cache.add"},
|
||||
{BLOB_DB_CACHE_ADD_FAILURES, "rocksdb.blobdb.cache.add.failures"},
|
||||
{BLOB_DB_CACHE_BYTES_READ, "rocksdb.blobdb.cache.bytes.read"},
|
||||
{BLOB_DB_CACHE_BYTES_WRITE, "rocksdb.blobdb.cache.bytes.write"}};
|
||||
{BLOB_DB_CACHE_BYTES_WRITE, "rocksdb.blobdb.cache.bytes.write"},
|
||||
{READ_ASYNC_MICROS, "rocksdb.read.async.micros"},
|
||||
{ASYNC_READ_ERROR_COUNT, "rocksdb.async.read.error.count"}};
|
||||
|
||||
const std::vector<std::pair<Histograms, std::string>> HistogramsNameMap = {
|
||||
{DB_GET, "rocksdb.db.get.micros"},
|
||||
|
||||
+17
-12
@@ -241,6 +241,23 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
if (*seq_ == kMaxSequenceNumber) {
|
||||
*seq_ = parsed_key.sequence;
|
||||
}
|
||||
if (max_covering_tombstone_seq_) {
|
||||
*seq_ = std::max(*seq_, *max_covering_tombstone_seq_);
|
||||
}
|
||||
}
|
||||
|
||||
size_t ts_sz = ucmp_->timestamp_size();
|
||||
if (ts_sz > 0 && timestamp_ != nullptr) {
|
||||
if (!timestamp_->empty()) {
|
||||
assert(ts_sz == timestamp_->size());
|
||||
}
|
||||
// TODO optimize for small size ts
|
||||
const std::string kMaxTs(ts_sz, '\xff');
|
||||
if (timestamp_->empty() ||
|
||||
ucmp_->CompareTimestamp(*timestamp_, kMaxTs) == 0) {
|
||||
Slice ts = ExtractTimestampFromUserKey(parsed_key.user_key, ts_sz);
|
||||
timestamp_->assign(ts.data(), ts.size());
|
||||
}
|
||||
}
|
||||
|
||||
auto type = parsed_key.type;
|
||||
@@ -359,13 +376,6 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state_ == kFound) {
|
||||
size_t ts_sz = ucmp_->timestamp_size();
|
||||
if (ts_sz > 0 && timestamp_ != nullptr) {
|
||||
Slice ts = ExtractTimestampFromUserKey(parsed_key.user_key, ts_sz);
|
||||
timestamp_->assign(ts.data(), ts.size());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
case kTypeDeletion:
|
||||
@@ -377,11 +387,6 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
assert(state_ == kNotFound || state_ == kMerge);
|
||||
if (kNotFound == state_) {
|
||||
state_ = kDeleted;
|
||||
size_t ts_sz = ucmp_->timestamp_size();
|
||||
if (ts_sz > 0 && timestamp_ != nullptr) {
|
||||
Slice ts = ExtractTimestampFromUserKey(parsed_key.user_key, ts_sz);
|
||||
timestamp_->assign(ts.data(), ts.size());
|
||||
}
|
||||
} else if (kMerge == state_) {
|
||||
state_ = kFound;
|
||||
Merge(nullptr);
|
||||
|
||||
+98
-36
@@ -117,14 +117,16 @@ class MergingIterator : public InternalIterator {
|
||||
public:
|
||||
MergingIterator(const InternalKeyComparator* comparator,
|
||||
InternalIterator** children, int n, bool is_arena_mode,
|
||||
bool prefix_seek_mode)
|
||||
bool prefix_seek_mode,
|
||||
const Slice* iterate_upper_bound = nullptr)
|
||||
: is_arena_mode_(is_arena_mode),
|
||||
prefix_seek_mode_(prefix_seek_mode),
|
||||
direction_(kForward),
|
||||
comparator_(comparator),
|
||||
current_(nullptr),
|
||||
minHeap_(comparator_),
|
||||
pinned_iters_mgr_(nullptr) {
|
||||
pinned_iters_mgr_(nullptr),
|
||||
iterate_upper_bound_(iterate_upper_bound) {
|
||||
children_.resize(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
children_[i].level = i;
|
||||
@@ -197,27 +199,48 @@ class MergingIterator : public InternalIterator {
|
||||
// Add range_tombstone_iters_[level] into min heap.
|
||||
// Updates active_ if the end key of a range tombstone is inserted.
|
||||
// @param start_key specifies which end point of the range tombstone to add.
|
||||
void InsertRangeTombstoneToMinHeap(size_t level, bool start_key = true) {
|
||||
void InsertRangeTombstoneToMinHeap(size_t level, bool start_key = true,
|
||||
bool replace_top = false) {
|
||||
assert(!range_tombstone_iters_.empty() &&
|
||||
range_tombstone_iters_[level]->Valid());
|
||||
if (start_key) {
|
||||
pinned_heap_item_[level].SetTombstoneKey(
|
||||
range_tombstone_iters_[level]->start_key());
|
||||
ParsedInternalKey pik = range_tombstone_iters_[level]->start_key();
|
||||
// iterate_upper_bound does not have timestamp
|
||||
if (iterate_upper_bound_ &&
|
||||
comparator_->user_comparator()->CompareWithoutTimestamp(
|
||||
pik.user_key, true /* a_has_ts */, *iterate_upper_bound_,
|
||||
false /* b_has_ts */) >= 0) {
|
||||
if (replace_top) {
|
||||
// replace_top implies this range tombstone iterator is still in
|
||||
// minHeap_ and at the top.
|
||||
minHeap_.pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
pinned_heap_item_[level].SetTombstoneKey(std::move(pik));
|
||||
pinned_heap_item_[level].type = HeapItem::DELETE_RANGE_START;
|
||||
assert(active_.count(level) == 0);
|
||||
} else {
|
||||
// allow end key to go over upper bound (if present) since start key is
|
||||
// before upper bound and the range tombstone could still cover a
|
||||
// range before upper bound.
|
||||
pinned_heap_item_[level].SetTombstoneKey(
|
||||
range_tombstone_iters_[level]->end_key());
|
||||
pinned_heap_item_[level].type = HeapItem::DELETE_RANGE_END;
|
||||
active_.insert(level);
|
||||
}
|
||||
minHeap_.push(&pinned_heap_item_[level]);
|
||||
if (replace_top) {
|
||||
minHeap_.replace_top(&pinned_heap_item_[level]);
|
||||
} else {
|
||||
minHeap_.push(&pinned_heap_item_[level]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add range_tombstone_iters_[level] into max heap.
|
||||
// Updates active_ if the start key of a range tombstone is inserted.
|
||||
// @param end_key specifies which end point of the range tombstone to add.
|
||||
void InsertRangeTombstoneToMaxHeap(size_t level, bool end_key = true) {
|
||||
void InsertRangeTombstoneToMaxHeap(size_t level, bool end_key = true,
|
||||
bool replace_top = false) {
|
||||
assert(!range_tombstone_iters_.empty() &&
|
||||
range_tombstone_iters_[level]->Valid());
|
||||
if (end_key) {
|
||||
@@ -231,7 +254,11 @@ class MergingIterator : public InternalIterator {
|
||||
pinned_heap_item_[level].type = HeapItem::DELETE_RANGE_START;
|
||||
active_.insert(level);
|
||||
}
|
||||
maxHeap_->push(&pinned_heap_item_[level]);
|
||||
if (replace_top) {
|
||||
maxHeap_->replace_top(&pinned_heap_item_[level]);
|
||||
} else {
|
||||
maxHeap_->push(&pinned_heap_item_[level]);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove HeapItems from top of minHeap_ that are of type DELETE_RANGE_START
|
||||
@@ -241,10 +268,10 @@ class MergingIterator : public InternalIterator {
|
||||
void PopDeleteRangeStart() {
|
||||
while (!minHeap_.empty() &&
|
||||
minHeap_.top()->type == HeapItem::DELETE_RANGE_START) {
|
||||
auto level = minHeap_.top()->level;
|
||||
minHeap_.pop();
|
||||
TEST_SYNC_POINT_CALLBACK("MergeIterator::PopDeleteRangeStart", nullptr);
|
||||
// insert end key of this range tombstone and updates active_
|
||||
InsertRangeTombstoneToMinHeap(level, false /* start_key */);
|
||||
InsertRangeTombstoneToMinHeap(
|
||||
minHeap_.top()->level, false /* start_key */, true /* replace_top */);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,10 +282,9 @@ class MergingIterator : public InternalIterator {
|
||||
void PopDeleteRangeEnd() {
|
||||
while (!maxHeap_->empty() &&
|
||||
maxHeap_->top()->type == HeapItem::DELETE_RANGE_END) {
|
||||
auto level = maxHeap_->top()->level;
|
||||
maxHeap_->pop();
|
||||
// insert start key of this range tombstone and updates active_
|
||||
InsertRangeTombstoneToMaxHeap(level, false /* end_key */);
|
||||
InsertRangeTombstoneToMaxHeap(maxHeap_->top()->level, false /* end_key */,
|
||||
true /* replace_top */);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,6 +591,10 @@ class MergingIterator : public InternalIterator {
|
||||
std::unique_ptr<MergerMaxIterHeap> maxHeap_;
|
||||
PinnedIteratorsManager* pinned_iters_mgr_;
|
||||
|
||||
// Used to bound range tombstones. For point keys, DBIter and SSTable iterator
|
||||
// take care of boundary checking.
|
||||
const Slice* iterate_upper_bound_;
|
||||
|
||||
// In forward direction, process a child that is not in the min heap.
|
||||
// If valid, add to the min heap. Otherwise, check status.
|
||||
void AddToMinHeapOrCheckStatus(HeapItem*);
|
||||
@@ -626,9 +656,19 @@ void MergingIterator::SeekImpl(const Slice& target, size_t starting_level,
|
||||
for (size_t level = 0; level < starting_level; ++level) {
|
||||
if (range_tombstone_iters_[level] &&
|
||||
range_tombstone_iters_[level]->Valid()) {
|
||||
assert(static_cast<bool>(active_.count(level)) ==
|
||||
(pinned_heap_item_[level].type == HeapItem::DELETE_RANGE_END));
|
||||
minHeap_.push(&pinned_heap_item_[level]);
|
||||
// use an iterator on active_ if performance becomes an issue here
|
||||
if (active_.count(level) > 0) {
|
||||
assert(pinned_heap_item_[level].type == HeapItem::DELETE_RANGE_END);
|
||||
// if it was active, then start key must be within upper_bound,
|
||||
// so we can add to minHeap_ directly.
|
||||
minHeap_.push(&pinned_heap_item_[level]);
|
||||
} else {
|
||||
// this takes care of checking iterate_upper_bound, but with an extra
|
||||
// key comparison if range_tombstone_iters_[level] was already out of
|
||||
// bound. Consider using a new HeapItem type or some flag to remember
|
||||
// boundary checking result.
|
||||
InsertRangeTombstoneToMinHeap(level);
|
||||
}
|
||||
} else {
|
||||
assert(!active_.count(level));
|
||||
}
|
||||
@@ -761,13 +801,15 @@ bool MergingIterator::SkipNextDeleted() {
|
||||
// - range deletion end key
|
||||
auto current = minHeap_.top();
|
||||
if (current->type == HeapItem::DELETE_RANGE_END) {
|
||||
minHeap_.pop();
|
||||
active_.erase(current->level);
|
||||
assert(range_tombstone_iters_[current->level] &&
|
||||
range_tombstone_iters_[current->level]->Valid());
|
||||
range_tombstone_iters_[current->level]->Next();
|
||||
if (range_tombstone_iters_[current->level]->Valid()) {
|
||||
InsertRangeTombstoneToMinHeap(current->level);
|
||||
InsertRangeTombstoneToMinHeap(current->level, true /* start_key */,
|
||||
true /* replace_top */);
|
||||
} else {
|
||||
minHeap_.pop();
|
||||
}
|
||||
return true /* current key deleted */;
|
||||
}
|
||||
@@ -977,13 +1019,15 @@ bool MergingIterator::SkipPrevDeleted() {
|
||||
// - range deletion start key
|
||||
auto current = maxHeap_->top();
|
||||
if (current->type == HeapItem::DELETE_RANGE_START) {
|
||||
maxHeap_->pop();
|
||||
active_.erase(current->level);
|
||||
assert(range_tombstone_iters_[current->level] &&
|
||||
range_tombstone_iters_[current->level]->Valid());
|
||||
range_tombstone_iters_[current->level]->Prev();
|
||||
if (range_tombstone_iters_[current->level]->Valid()) {
|
||||
InsertRangeTombstoneToMaxHeap(current->level);
|
||||
InsertRangeTombstoneToMaxHeap(current->level, true /* end_key */,
|
||||
true /* replace_top */);
|
||||
} else {
|
||||
maxHeap_->pop();
|
||||
}
|
||||
return true /* current key deleted */;
|
||||
}
|
||||
@@ -1266,11 +1310,12 @@ InternalIterator* NewMergingIterator(const InternalKeyComparator* cmp,
|
||||
}
|
||||
|
||||
MergeIteratorBuilder::MergeIteratorBuilder(
|
||||
const InternalKeyComparator* comparator, Arena* a, bool prefix_seek_mode)
|
||||
const InternalKeyComparator* comparator, Arena* a, bool prefix_seek_mode,
|
||||
const Slice* iterate_upper_bound)
|
||||
: first_iter(nullptr), use_merging_iter(false), arena(a) {
|
||||
auto mem = arena->AllocateAligned(sizeof(MergingIterator));
|
||||
merge_iter =
|
||||
new (mem) MergingIterator(comparator, nullptr, 0, true, prefix_seek_mode);
|
||||
merge_iter = new (mem) MergingIterator(comparator, nullptr, 0, true,
|
||||
prefix_seek_mode, iterate_upper_bound);
|
||||
}
|
||||
|
||||
MergeIteratorBuilder::~MergeIteratorBuilder() {
|
||||
@@ -1295,22 +1340,40 @@ void MergeIteratorBuilder::AddIterator(InternalIterator* iter) {
|
||||
}
|
||||
}
|
||||
|
||||
void MergeIteratorBuilder::AddRangeTombstoneIterator(
|
||||
TruncatedRangeDelIterator* iter, TruncatedRangeDelIterator*** iter_ptr) {
|
||||
if (!use_merging_iter) {
|
||||
void MergeIteratorBuilder::AddPointAndTombstoneIterator(
|
||||
InternalIterator* point_iter, TruncatedRangeDelIterator* tombstone_iter,
|
||||
TruncatedRangeDelIterator*** tombstone_iter_ptr) {
|
||||
// tombstone_iter_ptr != nullptr means point_iter is a LevelIterator.
|
||||
bool add_range_tombstone = tombstone_iter ||
|
||||
!merge_iter->range_tombstone_iters_.empty() ||
|
||||
tombstone_iter_ptr;
|
||||
if (!use_merging_iter && (add_range_tombstone || first_iter)) {
|
||||
use_merging_iter = true;
|
||||
if (first_iter) {
|
||||
merge_iter->AddIterator(first_iter);
|
||||
first_iter = nullptr;
|
||||
}
|
||||
}
|
||||
merge_iter->AddRangeTombstoneIterator(iter);
|
||||
if (iter_ptr) {
|
||||
// This is needed instead of setting to &range_tombstone_iters_[i] directly
|
||||
// here since the memory address of range_tombstone_iters_[i] might change
|
||||
// during vector resizing.
|
||||
range_del_iter_ptrs_.emplace_back(
|
||||
merge_iter->range_tombstone_iters_.size() - 1, iter_ptr);
|
||||
if (use_merging_iter) {
|
||||
merge_iter->AddIterator(point_iter);
|
||||
if (add_range_tombstone) {
|
||||
// If there was a gap, fill in nullptr as empty range tombstone iterators.
|
||||
while (merge_iter->range_tombstone_iters_.size() <
|
||||
merge_iter->children_.size() - 1) {
|
||||
merge_iter->AddRangeTombstoneIterator(nullptr);
|
||||
}
|
||||
merge_iter->AddRangeTombstoneIterator(tombstone_iter);
|
||||
}
|
||||
|
||||
if (tombstone_iter_ptr) {
|
||||
// This is needed instead of setting to &range_tombstone_iters_[i]
|
||||
// directly here since the memory address of range_tombstone_iters_[i]
|
||||
// might change during vector resizing.
|
||||
range_del_iter_ptrs_.emplace_back(
|
||||
merge_iter->range_tombstone_iters_.size() - 1, tombstone_iter_ptr);
|
||||
}
|
||||
} else {
|
||||
first_iter = point_iter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1323,8 +1386,7 @@ InternalIterator* MergeIteratorBuilder::Finish(ArenaWrappedDBIter* db_iter) {
|
||||
for (auto& p : range_del_iter_ptrs_) {
|
||||
*(p.second) = &(merge_iter->range_tombstone_iters_[p.first]);
|
||||
}
|
||||
if (db_iter) {
|
||||
assert(!merge_iter->range_tombstone_iters_.empty());
|
||||
if (db_iter && !merge_iter->range_tombstone_iters_.empty()) {
|
||||
// memtable is always the first level
|
||||
db_iter->SetMemtableRangetombstoneIter(
|
||||
&merge_iter->range_tombstone_iters_.front());
|
||||
|
||||
+18
-11
@@ -38,27 +38,34 @@ extern InternalIterator* NewMergingIterator(
|
||||
class MergingIterator;
|
||||
|
||||
// A builder class to build a merging iterator by adding iterators one by one.
|
||||
// User should call only one of AddIterator() or AddPointAndTombstoneIterator()
|
||||
// exclusively for the same builder.
|
||||
class MergeIteratorBuilder {
|
||||
public:
|
||||
// comparator: the comparator used in merging comparator
|
||||
// arena: where the merging iterator needs to be allocated from.
|
||||
explicit MergeIteratorBuilder(const InternalKeyComparator* comparator,
|
||||
Arena* arena, bool prefix_seek_mode = false);
|
||||
Arena* arena, bool prefix_seek_mode = false,
|
||||
const Slice* iterate_upper_bound = nullptr);
|
||||
~MergeIteratorBuilder();
|
||||
|
||||
// Add iter to the merging iterator.
|
||||
void AddIterator(InternalIterator* iter);
|
||||
|
||||
// Add a range tombstone iterator to underlying merge iterator.
|
||||
// See MergingIterator::AddRangeTombstoneIterator() for more detail.
|
||||
//
|
||||
// If `iter_ptr` is not nullptr, *iter_ptr will be set to where the merging
|
||||
// iterator stores `iter` when MergeIteratorBuilder::Finish() is called. This
|
||||
// is used by level iterator to update range tombstone iters when switching to
|
||||
// a different SST file.
|
||||
void AddRangeTombstoneIterator(
|
||||
TruncatedRangeDelIterator* iter,
|
||||
TruncatedRangeDelIterator*** iter_ptr = nullptr);
|
||||
// Add a point key iterator and a range tombstone iterator.
|
||||
// `tombstone_iter_ptr` should and only be set by LevelIterator.
|
||||
// *tombstone_iter_ptr will be set to where the merging iterator stores
|
||||
// `tombstone_iter` when MergeIteratorBuilder::Finish() is called. This is
|
||||
// used by LevelIterator to update range tombstone iters when switching to a
|
||||
// different SST file. If a single point iterator with a nullptr range
|
||||
// tombstone iterator is provided, and the point iterator is not a level
|
||||
// iterator, then this builder will return the point iterator directly,
|
||||
// instead of creating a merging iterator on top of it. Internally, if all
|
||||
// point iterators are not LevelIterator, then range tombstone iterator is
|
||||
// only added to the merging iter if there is a non-null `tombstone_iter`.
|
||||
void AddPointAndTombstoneIterator(
|
||||
InternalIterator* point_iter, TruncatedRangeDelIterator* tombstone_iter,
|
||||
TruncatedRangeDelIterator*** tombstone_iter_ptr = nullptr);
|
||||
|
||||
// Get arena used to build the merging iterator. It is called one a child
|
||||
// iterator needs to be allocated.
|
||||
|
||||
@@ -131,6 +131,8 @@ default_params = {
|
||||
"use_multiget" : lambda: random.randint(0, 1),
|
||||
"periodic_compaction_seconds" :
|
||||
lambda: random.choice([0, 0, 1, 2, 10, 100, 1000]),
|
||||
# 0 = never (used by some), 10 = often (for threading bugs), 600 = default
|
||||
"stats_dump_period_sec" : lambda: random.choice([0, 10, 600]),
|
||||
"compaction_ttl" : lambda: random.choice([0, 0, 1, 2, 10, 100, 1000]),
|
||||
# Test small max_manifest_file_size in a smaller chance, as most of the
|
||||
# time we wnat manifest history to be preserved to help debug
|
||||
|
||||
@@ -166,6 +166,65 @@ TEST_P(OptimisticTransactionTest, WriteConflictTest2) {
|
||||
delete txn;
|
||||
}
|
||||
|
||||
TEST_P(OptimisticTransactionTest, WriteConflictTest3) {
|
||||
ASSERT_OK(txn_db->Put(WriteOptions(), "foo", "bar"));
|
||||
|
||||
Transaction* txn = txn_db->BeginTransaction(WriteOptions());
|
||||
ASSERT_NE(txn, nullptr);
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(txn->GetForUpdate(ReadOptions(), "foo", &value));
|
||||
ASSERT_EQ(value, "bar");
|
||||
ASSERT_OK(txn->Merge("foo", "bar3"));
|
||||
|
||||
// Merge outside of a transaction should conflict with the previous merge
|
||||
ASSERT_OK(txn_db->Merge(WriteOptions(), "foo", "bar2"));
|
||||
ASSERT_OK(txn_db->Get(ReadOptions(), "foo", &value));
|
||||
ASSERT_EQ(value, "bar2");
|
||||
|
||||
ASSERT_EQ(1, txn->GetNumKeys());
|
||||
|
||||
Status s = txn->Commit();
|
||||
EXPECT_TRUE(s.IsBusy()); // Txn should not commit
|
||||
|
||||
// Verify that transaction did not write anything
|
||||
ASSERT_OK(txn_db->Get(ReadOptions(), "foo", &value));
|
||||
ASSERT_EQ(value, "bar2");
|
||||
|
||||
delete txn;
|
||||
}
|
||||
|
||||
TEST_P(OptimisticTransactionTest, WriteConflict4) {
|
||||
ASSERT_OK(txn_db->Put(WriteOptions(), "foo", "bar"));
|
||||
|
||||
Transaction* txn = txn_db->BeginTransaction(WriteOptions());
|
||||
ASSERT_NE(txn, nullptr);
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(txn->GetForUpdate(ReadOptions(), "foo", &value));
|
||||
ASSERT_EQ(value, "bar");
|
||||
ASSERT_OK(txn->Merge("foo", "bar3"));
|
||||
|
||||
// Range delete outside of a transaction should conflict with the previous
|
||||
// merge inside txn
|
||||
auto* dbimpl = static_cast_with_check<DBImpl>(txn_db->GetRootDB());
|
||||
ColumnFamilyHandle* default_cf = dbimpl->DefaultColumnFamily();
|
||||
ASSERT_OK(dbimpl->DeleteRange(WriteOptions(), default_cf, "foo", "foo1"));
|
||||
Status s = txn_db->Get(ReadOptions(), "foo", &value);
|
||||
ASSERT_TRUE(s.IsNotFound());
|
||||
|
||||
ASSERT_EQ(1, txn->GetNumKeys());
|
||||
|
||||
s = txn->Commit();
|
||||
EXPECT_TRUE(s.IsBusy()); // Txn should not commit
|
||||
|
||||
// Verify that transaction did not write anything
|
||||
s = txn_db->Get(ReadOptions(), "foo", &value);
|
||||
ASSERT_TRUE(s.IsNotFound());
|
||||
|
||||
delete txn;
|
||||
}
|
||||
|
||||
TEST_P(OptimisticTransactionTest, ReadConflictTest) {
|
||||
WriteOptions write_options;
|
||||
ReadOptions read_options, snapshot_read_options;
|
||||
|
||||
Reference in New Issue
Block a user