mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 03:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4595a5e95a | ||
|
|
d4d93a17d2 | ||
|
|
3798025699 | ||
|
|
9065ace05a | ||
|
|
5a06787a26 | ||
|
|
a1d8318563 | ||
|
|
eedf1fe068 | ||
|
|
80c4a67d6a | ||
|
|
5d0cf98e6c | ||
|
|
e3b5464785 | ||
|
|
7c48905ecd | ||
|
|
707e405492 | ||
|
|
340ac7ea6b | ||
|
|
d2fe0ee389 | ||
|
|
4951494a27 | ||
|
|
ac412b1095 | ||
|
|
9e14d06143 | ||
|
|
9c2c8f54fa | ||
|
|
42ba71fbbf |
No files matched your search
@@ -110,6 +110,20 @@ jobs:
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential libgflags-dev
|
||||
- run: make V=1 J=4 -j4 check
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-arm-crashtest:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential libgflags-dev libsnappy-dev zlib1g-dev libbz2-dev liblz4-dev libzstd-dev
|
||||
- run: sudo mount -o remount,size=16G /dev/shm
|
||||
- run: sudo dd bs=1048576 count=4096 if=/dev/zero of=/swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_atomic_flush
|
||||
- run: rm -rf /dev/shm/rocksdb.*
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_multiops_wc_txn
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-examples:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
|
||||
@@ -219,6 +219,10 @@ if(MSVC)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi /nologo /EHsc /GS /Gd /GR /GF /fp:precise /Zc:wchar_t /Zc:forScope /errorReport:queue")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FC /d2Zi+ /W4 /wd4127 /wd4996 /wd4100 /wd4324")
|
||||
endif()
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DNDEBUG")
|
||||
message(STATUS "Setting /DNDEBUG as CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
|
||||
endif()
|
||||
else()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W -Wextra -Wall -pthread")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-compare -Wshadow -Wno-unused-parameter -Wno-unused-variable -Woverloaded-virtual -Wnon-virtual-dtor -Wno-missing-field-initializers -Wno-strict-aliasing -Wno-invalid-offsetof")
|
||||
|
||||
+33
@@ -1,6 +1,39 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 10.10.1 (02/02/2026)
|
||||
### Bug Fixes
|
||||
* Fix Windows VS 2022 build errors.
|
||||
|
||||
## 10.10.0 (12/16/2025)
|
||||
### Bug Fixes
|
||||
* Fixed a bug in best-efforts recovery that causes use-after-free crashes when accessing SST files that were cached during the recovery.
|
||||
* Fix resumable compaction incorrectly allowing resumption from a truncated range deletion that is not well handled currently.
|
||||
* Fixed a bug in `PosixRandomFileAccess` IO uring submission queue ownership & management. Fix eliminates the false positive 'Bad cqe data' IO errors in `PosixRandomFileAccess::MultiRead` when interleaved with `PosixRandomFileAccess::ReadAsync` on the same thread.
|
||||
|
||||
## 10.9.0 (11/21/2025)
|
||||
### New Features
|
||||
* Added an auto-tuning feature for DB manifest file size that also (by default) improves the safety of existing configurations in case `max_manifest_file_size` is repeatedly exceeded. The new recommendation is to set `max_manifest_file_size` to something small like 1MB and tune `max_manifest_space_amp_pct` as needed to balance write amp and space amp in the manifest. Refer to comments on those options in `DBOptions` for details. Both options are (now) mutable.
|
||||
* Added a new API to support option migration for multiple column families
|
||||
* Added new option target_file_size_is_upper_bound that makes most compaction output SST files come close to the target file size without exceeding it, rather than commonly exceeding it by some fraction (current behavior). For now the new behavior is off by default, but we expect to enable it by default in the future.
|
||||
* Add a new option allow_trivial_move in CompactionOptions to allow CompactFiles to perform trivial move if possible. By default the flag of allow_trivial_move is false, so it preserve the original behavior.
|
||||
|
||||
### Public API Changes
|
||||
* To reduce risk of ODR violations or similar, `ROCKSDB_USING_THREAD_STATUS` has been removed from public headers and replaced with static `const bool ThreadStatus::kEnabled`. Some other uses of conditional compilation have been removed from public API headers to reduce risk of ODR violations or other issues.
|
||||
|
||||
### Behavior Changes
|
||||
* PosixWritableFile now repositions the seek pointer to the new end of file after a call to Truncate.
|
||||
* Updated standalone range deletion L0 file compaction behavior to avoid compacting with any newer L0 files (which is expensive and not useful).
|
||||
|
||||
### Bug Fixes
|
||||
* Fix a bug where compaction with range deletion can persist kTypeMaxValid in MANIFEST as file metadata. kTypeMaxValid is not supposed to be persisted and can change as new value types are introduced. This can cause a forward compatibility issue where older versions of RocksDB don't recognize kTypeMaxValid from newer versions. A new placeholder value type kTypeTruncatedRangeDeletionSentinel is also introduced to replace kTypeMaxValid when reading existing SST files' metadata from MANIFEST. This allows us to strengthen some checks to avoid using kTypeMaxValid in the future.
|
||||
* Fixed a bug where `DB::GetSortedWalFiles()` could hang when waiting for a purge operation that found nothing to do (potentially triggered by iterator release, flush, compaction, etc.).
|
||||
* Fixed a bug in MultiScan where `max_sequential_skip_in_iterations` could cause the iterator to seek backward to already-unpinned blocks when the same user key spans multiple data blocks, leading to assertion failures or seg fault.
|
||||
* Fixed a bug for `WAL_ttl_seconds > 0` use cases where the newest archived WAL files could be incorrectly deleted when the system clock moved backwards.
|
||||
|
||||
### Performance Improvements
|
||||
* Added optimization that allowed for the asynchronous prefetching of all data outlined in a multiscan iterator. This optimization was applied to the level iterator, which prefetches all data through each of the block-based iterators.
|
||||
|
||||
## 10.8.0 (10/21/2025)
|
||||
### New Features
|
||||
* Add kFSPrefetch to FSSupportedOps enum to allow file systems to indicate prefetch support capability, avoiding unnecessary prefetch system calls on file systems that don't support them.
|
||||
|
||||
@@ -296,6 +296,28 @@ $(info $(shell $(CC) --version))
|
||||
$(info $(shell $(CXX) --version))
|
||||
endif
|
||||
|
||||
# ccache support
|
||||
# Set USE_CCACHE=1 to enable ccache, or let it auto-detect
|
||||
ifndef USE_CCACHE
|
||||
CCACHE := $(shell which ccache 2>/dev/null)
|
||||
ifneq ($(CCACHE),)
|
||||
USE_CCACHE := 1
|
||||
else
|
||||
USE_CCACHE := 0
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(USE_CCACHE), 1)
|
||||
CCACHE := $(shell which ccache 2>/dev/null)
|
||||
ifneq ($(CCACHE),)
|
||||
$(info Using ccache: $(CCACHE))
|
||||
CC := $(CCACHE) $(CC)
|
||||
CXX := $(CCACHE) $(CXX)
|
||||
else
|
||||
$(warning ccache requested but not found in PATH)
|
||||
endif
|
||||
endif
|
||||
|
||||
missing_make_config_paths := $(shell \
|
||||
grep "\./\S*\|/\S*" -o $(CURDIR)/make_config.mk | \
|
||||
while read path; \
|
||||
|
||||
Vendored
+101
-124
@@ -1752,26 +1752,6 @@ inline void GetHomeIndexAndShift(uint64_t length_info, uint64_t hash,
|
||||
assert(*home < LengthInfoToUsedLength(length_info));
|
||||
}
|
||||
|
||||
inline int GetShiftFromNextWithShift(uint64_t next_with_shift) {
|
||||
return BitwiseAnd(next_with_shift,
|
||||
AutoHyperClockTable::HandleImpl::kShiftMask);
|
||||
}
|
||||
|
||||
inline size_t GetNextFromNextWithShift(uint64_t next_with_shift) {
|
||||
return static_cast<size_t>(next_with_shift >>
|
||||
AutoHyperClockTable::HandleImpl::kNextShift);
|
||||
}
|
||||
|
||||
inline uint64_t MakeNextWithShift(size_t next, int shift) {
|
||||
return (uint64_t{next} << AutoHyperClockTable::HandleImpl::kNextShift) |
|
||||
static_cast<uint64_t>(shift);
|
||||
}
|
||||
|
||||
inline uint64_t MakeNextWithShiftEnd(size_t head, int shift) {
|
||||
return AutoHyperClockTable::HandleImpl::kNextEndFlags |
|
||||
MakeNextWithShift(head, shift);
|
||||
}
|
||||
|
||||
// Helper function for Lookup
|
||||
inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
|
||||
int shift = 0, size_t home = 0,
|
||||
@@ -1821,36 +1801,39 @@ inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
|
||||
}
|
||||
}
|
||||
|
||||
using NextWithShift = AutoHyperClockTable::HandleImpl::NextWithShift;
|
||||
|
||||
// Assumes a chain rewrite lock prevents concurrent modification of
|
||||
// these chain pointers
|
||||
void UpgradeShiftsOnRange(AutoHyperClockTable::HandleImpl* arr,
|
||||
size_t& frontier, uint64_t stop_before_or_new_tail,
|
||||
int old_shift, int new_shift) {
|
||||
size_t& frontier,
|
||||
NextWithShift stop_before_or_new_tail, int old_shift,
|
||||
int new_shift) {
|
||||
assert(frontier != SIZE_MAX);
|
||||
assert(new_shift == old_shift + 1);
|
||||
(void)old_shift;
|
||||
(void)new_shift;
|
||||
using HandleImpl = AutoHyperClockTable::HandleImpl;
|
||||
for (;;) {
|
||||
uint64_t next_with_shift = arr[frontier].chain_next_with_shift.Load();
|
||||
assert(GetShiftFromNextWithShift(next_with_shift) == old_shift);
|
||||
NextWithShift next_with_shift = arr[frontier].chain_next_with_shift.Load();
|
||||
assert(next_with_shift.GetShift() == old_shift);
|
||||
if (next_with_shift == stop_before_or_new_tail) {
|
||||
// Stopping at entry with pointer matching "stop before"
|
||||
assert(!HandleImpl::IsEnd(next_with_shift));
|
||||
assert(!next_with_shift.IsEnd());
|
||||
return;
|
||||
}
|
||||
if (HandleImpl::IsEnd(next_with_shift)) {
|
||||
if (next_with_shift.IsEnd()) {
|
||||
// Also update tail to new tail
|
||||
assert(HandleImpl::IsEnd(stop_before_or_new_tail));
|
||||
assert(stop_before_or_new_tail.IsEnd());
|
||||
arr[frontier].chain_next_with_shift.Store(stop_before_or_new_tail);
|
||||
// Mark nothing left to upgrade
|
||||
frontier = SIZE_MAX;
|
||||
return;
|
||||
}
|
||||
// Next is another entry to process, so upgrade and advance frontier
|
||||
arr[frontier].chain_next_with_shift.FetchAdd(1U);
|
||||
assert(GetShiftFromNextWithShift(next_with_shift + 1) == new_shift);
|
||||
frontier = GetNextFromNextWithShift(next_with_shift);
|
||||
arr[frontier].chain_next_with_shift.Apply(
|
||||
NextWithShift::Shift::PlusTransformPromiseNoOverflow(1U));
|
||||
assert(next_with_shift.GetShift() + 1 == new_shift);
|
||||
frontier = next_with_shift.GetNext();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1888,19 +1871,19 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
// RAII wrap existing lock held (or end)
|
||||
explicit ChainRewriteLock(HandleImpl* h,
|
||||
RelaxedAtomic<uint64_t>& /*yield_count*/,
|
||||
uint64_t already_locked_or_end)
|
||||
NextWithShift already_locked_or_end)
|
||||
: head_ptr_(&h->head_next_with_shift) {
|
||||
saved_head_ = already_locked_or_end;
|
||||
// already locked or end
|
||||
assert(saved_head_ & HandleImpl::kHeadLocked);
|
||||
assert(saved_head_.IsLocked());
|
||||
}
|
||||
|
||||
~ChainRewriteLock() {
|
||||
if (!IsEnd()) {
|
||||
// Release lock
|
||||
uint64_t old = head_ptr_->FetchAnd(~HandleImpl::kHeadLocked);
|
||||
(void)old;
|
||||
assert((old & HandleImpl::kNextEndFlags) == HandleImpl::kHeadLocked);
|
||||
NextWithShift old;
|
||||
head_ptr_->Apply(NextWithShift::LockedFlag::ClearTransform(), &old);
|
||||
assert(old.IsLockedNotEnd());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1910,12 +1893,13 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
}
|
||||
|
||||
// Expected current state, assuming no parallel updates.
|
||||
uint64_t GetSavedHead() const { return saved_head_; }
|
||||
NextWithShift GetSavedHead() const { return saved_head_; }
|
||||
|
||||
bool CasUpdate(uint64_t next_with_shift,
|
||||
bool CasUpdate(NextWithShift next_with_shift,
|
||||
RelaxedAtomic<uint64_t>& yield_count) {
|
||||
uint64_t new_head = next_with_shift | HandleImpl::kHeadLocked;
|
||||
uint64_t expected = GetSavedHead();
|
||||
NextWithShift new_head =
|
||||
next_with_shift.With<NextWithShift::LockedFlag>(true);
|
||||
NextWithShift expected = GetSavedHead();
|
||||
bool success = head_ptr_->CasStrong(expected, new_head);
|
||||
if (success) {
|
||||
// Ensure IsEnd() is kept up-to-date, including for dtor
|
||||
@@ -1924,7 +1908,7 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
// Parallel update to head, such as Insert()
|
||||
if (IsEnd()) {
|
||||
// Didn't previously hold a lock
|
||||
if (HandleImpl::IsEnd(expected)) {
|
||||
if (expected.IsEnd()) {
|
||||
// Still don't need to
|
||||
saved_head_ = expected;
|
||||
} else {
|
||||
@@ -1933,28 +1917,25 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
}
|
||||
} else {
|
||||
// Parallel update must preserve our lock
|
||||
assert((expected & HandleImpl::kNextEndFlags) ==
|
||||
HandleImpl::kHeadLocked);
|
||||
assert(expected.IsLockedNotEnd());
|
||||
saved_head_ = expected;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool IsEnd() const { return HandleImpl::IsEnd(saved_head_); }
|
||||
bool IsEnd() const { return saved_head_.IsEnd(); }
|
||||
|
||||
private:
|
||||
void Acquire(RelaxedAtomic<uint64_t>& yield_count) {
|
||||
for (;;) {
|
||||
// Acquire removal lock on the chain
|
||||
uint64_t old_head = head_ptr_->FetchOr(HandleImpl::kHeadLocked);
|
||||
if ((old_head & HandleImpl::kNextEndFlags) != HandleImpl::kHeadLocked) {
|
||||
NextWithShift old_head;
|
||||
head_ptr_->Apply(NextWithShift::LockedFlag::SetTransform(), &old_head,
|
||||
&saved_head_);
|
||||
if (!old_head.IsLockedNotEnd()) {
|
||||
// Either acquired the lock or lock not needed (end)
|
||||
assert((old_head & HandleImpl::kNextEndFlags) == 0 ||
|
||||
(old_head & HandleImpl::kNextEndFlags) ==
|
||||
HandleImpl::kNextEndFlags);
|
||||
|
||||
saved_head_ = old_head | HandleImpl::kHeadLocked;
|
||||
assert(old_head.IsEnd() == old_head.IsLocked());
|
||||
break;
|
||||
}
|
||||
// NOTE: one of the few yield-wait loops, which is rare enough in practice
|
||||
@@ -1965,8 +1946,8 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
}
|
||||
}
|
||||
|
||||
AcqRelAtomic<uint64_t>* head_ptr_;
|
||||
uint64_t saved_head_;
|
||||
AcqRelBitFieldsAtomic<NextWithShift>* head_ptr_;
|
||||
NextWithShift saved_head_;
|
||||
};
|
||||
|
||||
AutoHyperClockTable::AutoHyperClockTable(
|
||||
@@ -2021,9 +2002,9 @@ AutoHyperClockTable::AutoHyperClockTable(
|
||||
#endif
|
||||
if (major + i < used_length) {
|
||||
array_[i].head_next_with_shift.StoreRelaxed(
|
||||
MakeNextWithShiftEnd(i, max_shift));
|
||||
NextWithShift::MakeEnd(i, max_shift));
|
||||
array_[major + i].head_next_with_shift.StoreRelaxed(
|
||||
MakeNextWithShiftEnd(major + i, max_shift));
|
||||
NextWithShift::MakeEnd(major + i, max_shift));
|
||||
#ifndef NDEBUG // Extra invariant checking
|
||||
GetHomeIndexAndShift(length_info, i, &home, &shift);
|
||||
assert(home == i);
|
||||
@@ -2034,7 +2015,7 @@ AutoHyperClockTable::AutoHyperClockTable(
|
||||
#endif
|
||||
} else {
|
||||
array_[i].head_next_with_shift.StoreRelaxed(
|
||||
MakeNextWithShiftEnd(i, min_shift));
|
||||
NextWithShift::MakeEnd(i, min_shift));
|
||||
#ifndef NDEBUG // Extra invariant checking
|
||||
GetHomeIndexAndShift(length_info, i, &home, &shift);
|
||||
assert(home == i);
|
||||
@@ -2066,8 +2047,10 @@ AutoHyperClockTable::~AutoHyperClockTable() {
|
||||
// just a reasonable frontier past what we expect to have written.
|
||||
#ifdef MUST_FREE_HEAP_ALLOCATIONS
|
||||
for (size_t i = used_end; i < array_.Count() && i < used_end + 64U; i++) {
|
||||
assert(array_[i].head_next_with_shift.LoadRelaxed() == 0);
|
||||
assert(array_[i].chain_next_with_shift.LoadRelaxed() == 0);
|
||||
assert(array_[i].head_next_with_shift.LoadRelaxed() ==
|
||||
HandleImpl::kUnusedMarker);
|
||||
assert(array_[i].chain_next_with_shift.LoadRelaxed() ==
|
||||
HandleImpl::kUnusedMarker);
|
||||
assert(array_[i].meta.LoadRelaxed() == 0);
|
||||
}
|
||||
#endif // MUST_FREE_HEAP_ALLOCATIONS
|
||||
@@ -2089,11 +2072,9 @@ AutoHyperClockTable::~AutoHyperClockTable() {
|
||||
usage_.FetchSubRelaxed(h.total_charge);
|
||||
occupancy_.FetchSubRelaxed(1U);
|
||||
was_populated[i] = true;
|
||||
if (!HandleImpl::IsEnd(h.chain_next_with_shift.LoadRelaxed())) {
|
||||
assert((h.chain_next_with_shift.LoadRelaxed() &
|
||||
HandleImpl::kHeadLocked) == 0);
|
||||
size_t next =
|
||||
GetNextFromNextWithShift(h.chain_next_with_shift.LoadRelaxed());
|
||||
if (!h.chain_next_with_shift.LoadRelaxed().IsEnd()) {
|
||||
assert(!h.chain_next_with_shift.LoadRelaxed().IsLocked());
|
||||
size_t next = h.chain_next_with_shift.LoadRelaxed().GetNext();
|
||||
assert(!was_pointed_to[next]);
|
||||
was_pointed_to[next] = true;
|
||||
}
|
||||
@@ -2105,9 +2086,8 @@ AutoHyperClockTable::~AutoHyperClockTable() {
|
||||
break;
|
||||
}
|
||||
#ifndef NDEBUG // Extra invariant checking
|
||||
if (!HandleImpl::IsEnd(h.head_next_with_shift.LoadRelaxed())) {
|
||||
size_t next =
|
||||
GetNextFromNextWithShift(h.head_next_with_shift.LoadRelaxed());
|
||||
if (!h.head_next_with_shift.LoadRelaxed().IsEnd()) {
|
||||
size_t next = h.head_next_with_shift.LoadRelaxed().GetNext();
|
||||
assert(!was_pointed_to[next]);
|
||||
was_pointed_to[next] = true;
|
||||
}
|
||||
@@ -2222,10 +2202,10 @@ bool AutoHyperClockTable::Grow(InsertState& state) {
|
||||
// chain rewrite lock has been released.
|
||||
size_t old_old_home = BottomNBits(grow_home, old_shift - 1);
|
||||
for (;;) {
|
||||
uint64_t old_old_head = array_[old_old_home].head_next_with_shift.Load();
|
||||
if (GetShiftFromNextWithShift(old_old_head) >= old_shift) {
|
||||
if ((old_old_head & HandleImpl::kNextEndFlags) !=
|
||||
HandleImpl::kHeadLocked) {
|
||||
NextWithShift old_old_head =
|
||||
array_[old_old_home].head_next_with_shift.Load();
|
||||
if (old_old_head.GetShift() >= old_shift) {
|
||||
if (!old_old_head.IsLockedNotEnd()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2285,8 +2265,7 @@ void AutoHyperClockTable::CatchUpLengthInfoNoWait(
|
||||
if (published_usable_size < known_usable_grow_home) {
|
||||
int old_shift = FloorLog2(next_usable_size - 1);
|
||||
size_t old_home = BottomNBits(published_usable_size, old_shift);
|
||||
int shift = GetShiftFromNextWithShift(
|
||||
array_[old_home].head_next_with_shift.Load());
|
||||
int shift = array_[old_home].head_next_with_shift.Load().GetShift();
|
||||
if (shift <= old_shift) {
|
||||
// Not ready
|
||||
break;
|
||||
@@ -2437,9 +2416,10 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
ChainRewriteLock zero_head_lock(&arr[old_home], yield_count_);
|
||||
|
||||
// Used for locking the one chain below
|
||||
uint64_t saved_one_head;
|
||||
NextWithShift saved_one_head;
|
||||
// One head has not been written to
|
||||
assert(arr[grow_home].head_next_with_shift.Load() == 0);
|
||||
assert(arr[grow_home].head_next_with_shift.Load() ==
|
||||
HandleImpl::kUnusedMarker);
|
||||
|
||||
// old_home will also the head of the new "zero chain" -- all entries in the
|
||||
// "from" chain whose next hash bit is 0. grow_home will be head of the new
|
||||
@@ -2461,7 +2441,7 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
assert(cur == SIZE_MAX);
|
||||
assert(chain_frontier_first == -1);
|
||||
|
||||
uint64_t next_with_shift = zero_head_lock.GetSavedHead();
|
||||
NextWithShift next_with_shift = zero_head_lock.GetSavedHead();
|
||||
|
||||
// Find a single representative for each target chain, or scan the whole
|
||||
// chain if some target chain has no representative.
|
||||
@@ -2474,16 +2454,16 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
assert((cur == SIZE_MAX) == (zero_chain_frontier == SIZE_MAX &&
|
||||
one_chain_frontier == SIZE_MAX));
|
||||
|
||||
assert(GetShiftFromNextWithShift(next_with_shift) == old_shift);
|
||||
assert(next_with_shift.GetShift() == old_shift);
|
||||
|
||||
// Check for end of original chain
|
||||
if (HandleImpl::IsEnd(next_with_shift)) {
|
||||
if (next_with_shift.IsEnd()) {
|
||||
cur = SIZE_MAX;
|
||||
break;
|
||||
}
|
||||
|
||||
// next_with_shift is not End
|
||||
cur = GetNextFromNextWithShift(next_with_shift);
|
||||
cur = next_with_shift.GetNext();
|
||||
|
||||
if (BottomNBits(arr[cur].hashed_key[1], new_shift) == old_home) {
|
||||
// Entry for zero chain
|
||||
@@ -2522,10 +2502,10 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
(zero_chain_frontier == SIZE_MAX && one_chain_frontier == SIZE_MAX));
|
||||
|
||||
// Always update one chain's head first (safe), and mark it as locked
|
||||
saved_one_head = HandleImpl::kHeadLocked |
|
||||
(one_chain_frontier != SIZE_MAX
|
||||
? MakeNextWithShift(one_chain_frontier, new_shift)
|
||||
: MakeNextWithShiftEnd(grow_home, new_shift));
|
||||
saved_one_head = one_chain_frontier != SIZE_MAX
|
||||
? NextWithShift::Make(one_chain_frontier, new_shift)
|
||||
: NextWithShift::MakeEnd(grow_home, new_shift);
|
||||
saved_one_head.Set<NextWithShift::LockedFlag>(true);
|
||||
arr[grow_home].head_next_with_shift.Store(saved_one_head);
|
||||
|
||||
// Make sure length_info_ hasn't been updated too early, as we're about
|
||||
@@ -2535,8 +2515,8 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
// Try to set zero's head.
|
||||
if (zero_head_lock.CasUpdate(
|
||||
zero_chain_frontier != SIZE_MAX
|
||||
? MakeNextWithShift(zero_chain_frontier, new_shift)
|
||||
: MakeNextWithShiftEnd(old_home, new_shift),
|
||||
? NextWithShift::Make(zero_chain_frontier, new_shift)
|
||||
: NextWithShift::MakeEnd(old_home, new_shift),
|
||||
yield_count_)) {
|
||||
// Both heads successfully updated to new shift
|
||||
break;
|
||||
@@ -2570,10 +2550,10 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
size_t& other_frontier = chain_frontier_first != 0
|
||||
? /*&*/ zero_chain_frontier
|
||||
: /*&*/ one_chain_frontier;
|
||||
uint64_t stop_before_or_new_tail =
|
||||
NextWithShift stop_before_or_new_tail =
|
||||
other_frontier != SIZE_MAX
|
||||
? /*stop before*/ MakeNextWithShift(other_frontier, old_shift)
|
||||
: /*new tail*/ MakeNextWithShiftEnd(
|
||||
? /*stop before*/ NextWithShift::Make(other_frontier, old_shift)
|
||||
: /*new tail*/ NextWithShift::MakeEnd(
|
||||
chain_frontier_first == 0 ? old_home : grow_home, new_shift);
|
||||
UpgradeShiftsOnRange(arr, first_frontier, stop_before_or_new_tail,
|
||||
old_shift, new_shift);
|
||||
@@ -2599,20 +2579,19 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
? /*&*/ zero_chain_frontier
|
||||
: /*&*/ one_chain_frontier;
|
||||
assert(cur != first_frontier);
|
||||
assert(GetNextFromNextWithShift(
|
||||
arr[first_frontier].chain_next_with_shift.Load()) ==
|
||||
assert(arr[first_frontier].chain_next_with_shift.Load().GetNext() ==
|
||||
other_frontier);
|
||||
|
||||
uint64_t next_with_shift = arr[cur].chain_next_with_shift.Load();
|
||||
NextWithShift next_with_shift = arr[cur].chain_next_with_shift.Load();
|
||||
|
||||
// Check for end of original chain
|
||||
if (HandleImpl::IsEnd(next_with_shift)) {
|
||||
if (next_with_shift.IsEnd()) {
|
||||
// Can set upgraded tail on first chain
|
||||
uint64_t first_new_tail = MakeNextWithShiftEnd(
|
||||
NextWithShift first_new_tail = NextWithShift::MakeEnd(
|
||||
chain_frontier_first == 0 ? old_home : grow_home, new_shift);
|
||||
arr[first_frontier].chain_next_with_shift.Store(first_new_tail);
|
||||
// And upgrade remainder of other chain
|
||||
uint64_t other_new_tail = MakeNextWithShiftEnd(
|
||||
NextWithShift other_new_tail = NextWithShift::MakeEnd(
|
||||
chain_frontier_first != 0 ? old_home : grow_home, new_shift);
|
||||
UpgradeShiftsOnRange(arr, other_frontier, other_new_tail, old_shift,
|
||||
new_shift);
|
||||
@@ -2621,7 +2600,7 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
}
|
||||
|
||||
// next_with_shift is not End
|
||||
cur = GetNextFromNextWithShift(next_with_shift);
|
||||
cur = next_with_shift.GetNext();
|
||||
|
||||
int target_chain;
|
||||
if (BottomNBits(arr[cur].hashed_key[1], new_shift) == old_home) {
|
||||
@@ -2634,7 +2613,7 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
}
|
||||
if (target_chain == chain_frontier_first) {
|
||||
// Found next entry to skip to on the first chain
|
||||
uint64_t skip_to = MakeNextWithShift(cur, new_shift);
|
||||
NextWithShift skip_to = NextWithShift::Make(cur, new_shift);
|
||||
arr[first_frontier].chain_next_with_shift.Store(skip_to);
|
||||
first_frontier = cur;
|
||||
// Upgrade other chain up to entry before that one
|
||||
@@ -2675,17 +2654,17 @@ void AutoHyperClockTable::PurgeImplLocked(OpData* op_data,
|
||||
|
||||
HandleImpl* const arr = array_.Get();
|
||||
|
||||
uint64_t next_with_shift = rewrite_lock.GetSavedHead();
|
||||
assert(!HandleImpl::IsEnd(next_with_shift));
|
||||
int home_shift = GetShiftFromNextWithShift(next_with_shift);
|
||||
NextWithShift next_with_shift = rewrite_lock.GetSavedHead();
|
||||
assert(!next_with_shift.IsEnd());
|
||||
int home_shift = next_with_shift.GetShift();
|
||||
(void)home;
|
||||
(void)home_shift;
|
||||
size_t next = GetNextFromNextWithShift(next_with_shift);
|
||||
size_t next = next_with_shift.GetNext();
|
||||
assert(next < array_.Count());
|
||||
HandleImpl* h = &arr[next];
|
||||
HandleImpl* prev_to_keep = nullptr;
|
||||
#ifndef NDEBUG
|
||||
uint64_t prev_to_keep_next_with_shift = 0;
|
||||
NextWithShift prev_to_keep_next_with_shift{};
|
||||
#endif
|
||||
// Whether there are entries between h and prev_to_keep that should be
|
||||
// purged from the chain.
|
||||
@@ -2743,13 +2722,13 @@ void AutoHyperClockTable::PurgeImplLocked(OpData* op_data,
|
||||
// update any new entries just inserted in parallel.
|
||||
// Can simply restart (GetSavedHead() already updated from CAS failure).
|
||||
next_with_shift = rewrite_lock.GetSavedHead();
|
||||
assert(!HandleImpl::IsEnd(next_with_shift));
|
||||
next = GetNextFromNextWithShift(next_with_shift);
|
||||
assert(!next_with_shift.IsEnd());
|
||||
next = next_with_shift.GetNext();
|
||||
assert(next < array_.Count());
|
||||
h = &arr[next];
|
||||
pending_purge = false;
|
||||
assert(prev_to_keep == nullptr);
|
||||
assert(GetShiftFromNextWithShift(next_with_shift) == home_shift);
|
||||
assert(next_with_shift.GetShift() == home_shift);
|
||||
continue;
|
||||
}
|
||||
pending_purge = false;
|
||||
@@ -2771,13 +2750,13 @@ void AutoHyperClockTable::PurgeImplLocked(OpData* op_data,
|
||||
}
|
||||
#endif
|
||||
|
||||
assert(GetShiftFromNextWithShift(next_with_shift) == home_shift);
|
||||
assert(next_with_shift.GetShift() == home_shift);
|
||||
|
||||
// Check for end marker
|
||||
if (HandleImpl::IsEnd(next_with_shift)) {
|
||||
if (next_with_shift.IsEnd()) {
|
||||
h = nullptr;
|
||||
} else {
|
||||
next = GetNextFromNextWithShift(next_with_shift);
|
||||
next = next_with_shift.GetNext();
|
||||
assert(next < array_.Count());
|
||||
h = &arr[next];
|
||||
assert(h != prev_to_keep);
|
||||
@@ -2849,7 +2828,7 @@ void AutoHyperClockTable::PurgeImpl(OpData* op_data, size_t home,
|
||||
// Ensure we are at the correct home for the shift in effect for the
|
||||
// chain head.
|
||||
for (;;) {
|
||||
int shift = GetShiftFromNextWithShift(rewrite_lock.GetSavedHead());
|
||||
int shift = rewrite_lock.GetSavedHead().GetShift();
|
||||
|
||||
if (shift > home_shift) {
|
||||
// Found a newer shift at candidate head, which must apply to us.
|
||||
@@ -3045,14 +3024,14 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::DoInsert(
|
||||
}
|
||||
|
||||
// Now insert into chain using head pointer
|
||||
uint64_t next_with_shift;
|
||||
NextWithShift next_with_shift;
|
||||
int home_shift = orig_home_shift;
|
||||
|
||||
// Might need to retry
|
||||
for (int i = 0;; ++i) {
|
||||
CHECK_TOO_MANY_ITERATIONS(i);
|
||||
next_with_shift = arr[home].head_next_with_shift.Load();
|
||||
int shift = GetShiftFromNextWithShift(next_with_shift);
|
||||
int shift = next_with_shift.GetShift();
|
||||
|
||||
if (UNLIKELY(shift != home_shift)) {
|
||||
// NOTE: shift increases with table growth
|
||||
@@ -3079,15 +3058,14 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::DoInsert(
|
||||
}
|
||||
|
||||
// Values to update to
|
||||
uint64_t head_next_with_shift = MakeNextWithShift(idx, home_shift);
|
||||
uint64_t chain_next_with_shift = next_with_shift;
|
||||
NextWithShift head_next_with_shift = NextWithShift::Make(idx, home_shift);
|
||||
NextWithShift chain_next_with_shift = next_with_shift;
|
||||
|
||||
// Preserve the locked state in head, without propagating to chain next
|
||||
// where it is meaningless (and not allowed)
|
||||
if (UNLIKELY((next_with_shift & HandleImpl::kNextEndFlags) ==
|
||||
HandleImpl::kHeadLocked)) {
|
||||
head_next_with_shift |= HandleImpl::kHeadLocked;
|
||||
chain_next_with_shift &= ~HandleImpl::kHeadLocked;
|
||||
if (UNLIKELY(next_with_shift.IsLockedNotEnd())) {
|
||||
head_next_with_shift.Set<NextWithShift::LockedFlag>(true);
|
||||
chain_next_with_shift.Set<NextWithShift::LockedFlag>(false);
|
||||
}
|
||||
|
||||
arr[idx].chain_next_with_shift.Store(chain_next_with_shift);
|
||||
@@ -3156,9 +3134,9 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
|
||||
// of a loop as possible.
|
||||
|
||||
HandleImpl* const arr = array_.Get();
|
||||
uint64_t next_with_shift = arr[home].head_next_with_shift.LoadRelaxed();
|
||||
for (size_t i = 0; !HandleImpl::IsEnd(next_with_shift) && i < 10; ++i) {
|
||||
HandleImpl* h = &arr[GetNextFromNextWithShift(next_with_shift)];
|
||||
NextWithShift next_with_shift = arr[home].head_next_with_shift.LoadRelaxed();
|
||||
for (size_t i = 0; !next_with_shift.IsEnd() && i < 10; ++i) {
|
||||
HandleImpl* h = &arr[next_with_shift.IsEnd()];
|
||||
// Attempt cheap key match without acquiring a read ref. This could give a
|
||||
// false positive, which is re-checked after acquiring read ref, or false
|
||||
// negative, which is re-checked in the full Lookup. Also, this is a
|
||||
@@ -3203,7 +3181,7 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
|
||||
// Read head or chain pointer
|
||||
next_with_shift = h ? h->chain_next_with_shift.Load()
|
||||
: arr[home].head_next_with_shift.Load();
|
||||
int shift = GetShiftFromNextWithShift(next_with_shift);
|
||||
int shift = next_with_shift.GetShift();
|
||||
|
||||
// Make sure it's usable
|
||||
size_t effective_home = home;
|
||||
@@ -3257,10 +3235,10 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
|
||||
}
|
||||
|
||||
// Check for end marker
|
||||
if (HandleImpl::IsEnd(next_with_shift)) {
|
||||
if (next_with_shift.IsEnd()) {
|
||||
// To ensure we didn't miss anything in the chain, the end marker must
|
||||
// point back to the correct home.
|
||||
if (LIKELY(GetNextFromNextWithShift(next_with_shift) == effective_home)) {
|
||||
if (LIKELY(next_with_shift.GetNext() == effective_home)) {
|
||||
// Complete, clean iteration of the chain, not found.
|
||||
// Clean up.
|
||||
if (read_ref_on_chain) {
|
||||
@@ -3276,7 +3254,7 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
|
||||
}
|
||||
|
||||
// Follow the next and check for full key match, home match, or neither
|
||||
h = &arr[GetNextFromNextWithShift(next_with_shift)];
|
||||
h = &arr[next_with_shift.GetNext()];
|
||||
bool full_match_or_unknown = false;
|
||||
if (MatchAndRef(&hashed_key, *h, shift, effective_home,
|
||||
&full_match_or_unknown)) {
|
||||
@@ -3600,8 +3578,7 @@ size_t AutoHyperClockTable::CalcMaxUsableLength(
|
||||
|
||||
namespace {
|
||||
bool IsHeadNonempty(const AutoHyperClockTable::HandleImpl& h) {
|
||||
return !AutoHyperClockTable::HandleImpl::IsEnd(
|
||||
h.head_next_with_shift.LoadRelaxed());
|
||||
return !h.head_next_with_shift.LoadRelaxed().IsEnd();
|
||||
}
|
||||
bool IsEntryAtHome(const AutoHyperClockTable::HandleImpl& h, int shift,
|
||||
size_t home) {
|
||||
|
||||
Vendored
+44
-27
@@ -775,6 +775,7 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
// chain--specifically the next entry in the chain.
|
||||
// * The end of a chain is given a special "end" marker and refers back
|
||||
// to the head of the chain.
|
||||
// These decorated pointers use the NextWithShift bit field struct below.
|
||||
//
|
||||
// Why do we need shift on each pointer? To make Lookup wait-free, we need
|
||||
// to be able to query a chain without missing anything, and preferably
|
||||
@@ -794,47 +795,63 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
// it is normal to see "under construction" entries on the chain, and it
|
||||
// is not safe to read their hashed key without either a read reference
|
||||
// on the entry or a rewrite lock on the chain.
|
||||
struct NextWithShift : public BitFields<uint64_t, NextWithShift> {
|
||||
// The "shift" associated with this decorated pointer (see description
|
||||
// above).
|
||||
using Shift = UnsignedBitField<NextWithShift, 6, NoPrevBitField>;
|
||||
// Marker for the end of a chain. Must also (a) point back to the head of
|
||||
// the chain (with end marker removed), and (b) set the LockedFlag
|
||||
// (below), so that attempting to lock an empty chain has no effect (not
|
||||
// needed, as the lock is only needed for removals).
|
||||
using EndFlag = BoolBitField<NextWithShift, Shift>;
|
||||
// Marker that some thread owning writes to the chain structure (except
|
||||
// for inserts), but only if not an "end" pointer. Also called the
|
||||
// "rewrite lock."
|
||||
using LockedFlag = BoolBitField<NextWithShift, EndFlag>;
|
||||
// The "next" associated with this decorated pointer, which is an index
|
||||
// into the table's array_ (see description above).
|
||||
using Next = UnsignedBitField<NextWithShift, 56, LockedFlag>;
|
||||
|
||||
// Marker in a "with_shift" head pointer for some thread owning writes
|
||||
// to the chain structure (except for inserts), but only if not an
|
||||
// "end" pointer. Also called the "rewrite lock."
|
||||
static constexpr uint64_t kHeadLocked = uint64_t{1} << 7;
|
||||
bool IsLocked() const { return Get<LockedFlag>(); }
|
||||
bool IsEnd() const {
|
||||
// End flag should imply locked flag
|
||||
assert(!Get<EndFlag>() || Get<LockedFlag>());
|
||||
return Get<EndFlag>();
|
||||
}
|
||||
bool IsLockedNotEnd() const {
|
||||
// NOTE: helping GCC to optimize this simpler code:
|
||||
// return IsLocked() && !IsEnd();
|
||||
constexpr U kEndFlag = U{1} << EndFlag::kBitOffset;
|
||||
constexpr U kLockedFlag = U{1} << LockedFlag::kBitOffset;
|
||||
return (underlying & (kEndFlag | kLockedFlag)) == kLockedFlag;
|
||||
}
|
||||
auto GetNext() const { return Get<Next>(); }
|
||||
auto GetShift() const { return Get<Shift>(); }
|
||||
|
||||
// Marker in a "with_shift" pointer for the end of a chain. Must also
|
||||
// point back to the head of the chain (with end marker removed).
|
||||
// Also includes the "locked" bit so that attempting to lock an empty
|
||||
// chain has no effect (not needed, as the lock is only needed for
|
||||
// removals).
|
||||
static constexpr uint64_t kNextEndFlags = (uint64_t{1} << 6) | kHeadLocked;
|
||||
static NextWithShift Make(size_t next, int shift) {
|
||||
return NextWithShift{}.With<Next>(next).With<Shift>(
|
||||
static_cast<uint8_t>(shift));
|
||||
}
|
||||
|
||||
static inline bool IsEnd(uint64_t next_with_shift) {
|
||||
// Assuming certain values never used, suffices to check this one bit
|
||||
constexpr auto kCheckBit = kNextEndFlags ^ kHeadLocked;
|
||||
return next_with_shift & kCheckBit;
|
||||
}
|
||||
|
||||
// Bottom bits to right shift away to get an array index from a
|
||||
// "with_shift" pointer.
|
||||
static constexpr int kNextShift = 8;
|
||||
|
||||
// A bit mask for the "shift" associated with each "with_shift" pointer.
|
||||
// Always bottommost bits.
|
||||
static constexpr int kShiftMask = 63;
|
||||
static NextWithShift MakeEnd(size_t next, int shift) {
|
||||
return Make(next, shift).With<EndFlag>(true).With<LockedFlag>(true);
|
||||
}
|
||||
};
|
||||
|
||||
// A marker for head_next_with_shift that indicates this HandleImpl is
|
||||
// heap allocated (standalone) rather than in the table.
|
||||
static constexpr uint64_t kStandaloneMarker = UINT64_MAX;
|
||||
static constexpr NextWithShift kStandaloneMarker{UINT64_MAX};
|
||||
|
||||
// A marker for head_next_with_shift indicating the head is not yet part
|
||||
// of the usable table, or for chain_next_with_shift indicating that the
|
||||
// entry is not present or is not yet part of a chain (must not be
|
||||
// "shareable" state).
|
||||
static constexpr uint64_t kUnusedMarker = 0;
|
||||
static constexpr NextWithShift kUnusedMarker{0};
|
||||
|
||||
// See above. The head pointer is logically independent of the rest of
|
||||
// the entry, including the chain next pointer.
|
||||
AcqRelAtomic<uint64_t> head_next_with_shift{kUnusedMarker};
|
||||
AcqRelAtomic<uint64_t> chain_next_with_shift{kUnusedMarker};
|
||||
AcqRelBitFieldsAtomic<NextWithShift> head_next_with_shift{kUnusedMarker};
|
||||
AcqRelBitFieldsAtomic<NextWithShift> chain_next_with_shift{kUnusedMarker};
|
||||
|
||||
// For supporting CreateStandalone and some fallback cases.
|
||||
inline bool IsStandalone() const {
|
||||
|
||||
@@ -71,6 +71,11 @@ using ROCKSDB_NAMESPACE::CompactionFilterFactory;
|
||||
using ROCKSDB_NAMESPACE::CompactionJobInfo;
|
||||
using ROCKSDB_NAMESPACE::CompactionOptionsFIFO;
|
||||
using ROCKSDB_NAMESPACE::CompactionReason;
|
||||
using ROCKSDB_NAMESPACE::CompactionService;
|
||||
using ROCKSDB_NAMESPACE::CompactionServiceJobInfo;
|
||||
using ROCKSDB_NAMESPACE::CompactionServiceJobStatus;
|
||||
using ROCKSDB_NAMESPACE::CompactionServiceOptionsOverride;
|
||||
using ROCKSDB_NAMESPACE::CompactionServiceScheduleResponse;
|
||||
using ROCKSDB_NAMESPACE::CompactRangeOptions;
|
||||
using ROCKSDB_NAMESPACE::Comparator;
|
||||
using ROCKSDB_NAMESPACE::CompressionType;
|
||||
@@ -107,6 +112,7 @@ using ROCKSDB_NAMESPACE::NewCompactOnDeletionCollectorFactory;
|
||||
using ROCKSDB_NAMESPACE::NewGenericRateLimiter;
|
||||
using ROCKSDB_NAMESPACE::NewLRUCache;
|
||||
using ROCKSDB_NAMESPACE::NewRibbonFilterPolicy;
|
||||
using ROCKSDB_NAMESPACE::OpenAndCompactOptions;
|
||||
using ROCKSDB_NAMESPACE::OptimisticTransactionDB;
|
||||
using ROCKSDB_NAMESPACE::OptimisticTransactionOptions;
|
||||
using ROCKSDB_NAMESPACE::Options;
|
||||
@@ -349,6 +355,22 @@ struct rocksdb_statistics_histogram_data_t {
|
||||
HistogramData rep;
|
||||
};
|
||||
|
||||
struct rocksdb_compactionservice_scheduleresponse_t {
|
||||
CompactionServiceScheduleResponse rep;
|
||||
};
|
||||
|
||||
struct rocksdb_compactionservice_jobinfo_t {
|
||||
CompactionServiceJobInfo rep;
|
||||
};
|
||||
|
||||
struct rocksdb_compaction_service_options_override_t {
|
||||
CompactionServiceOptionsOverride rep;
|
||||
};
|
||||
|
||||
struct rocksdb_open_and_compact_options_t {
|
||||
OpenAndCompactOptions rep;
|
||||
};
|
||||
|
||||
struct rocksdb_compactionfilter_t : public CompactionFilter {
|
||||
void* state_;
|
||||
void (*destructor_)(void*);
|
||||
@@ -643,6 +665,410 @@ static inline char* CopyString(const Slice& slice) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* rocksdb_compactionservice_jobinfo_t_get_db_name(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len) {
|
||||
*len = info->rep.db_name.size();
|
||||
return info->rep.db_name.data();
|
||||
}
|
||||
|
||||
const char* rocksdb_compactionservice_jobinfo_t_get_db_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len) {
|
||||
*len = info->rep.db_id.size();
|
||||
return info->rep.db_id.data();
|
||||
}
|
||||
|
||||
const char* rocksdb_compactionservice_jobinfo_t_get_db_session_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len) {
|
||||
*len = info->rep.db_session_id.size();
|
||||
return info->rep.db_session_id.data();
|
||||
}
|
||||
|
||||
const char* rocksdb_compactionservice_jobinfo_t_get_cf_name(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len) {
|
||||
*len = info->rep.cf_name.size();
|
||||
return info->rep.cf_name.data();
|
||||
}
|
||||
|
||||
uint32_t rocksdb_compactionservice_jobinfo_t_get_cf_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.cf_id;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_compactionservice_jobinfo_t_get_job_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.job_id;
|
||||
}
|
||||
|
||||
int rocksdb_compactionservice_jobinfo_t_get_priority(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return static_cast<int>(info->rep.priority);
|
||||
}
|
||||
|
||||
int rocksdb_compactionservice_jobinfo_t_get_compaction_reason(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return static_cast<int>(info->rep.compaction_reason);
|
||||
}
|
||||
|
||||
int rocksdb_compactionservice_jobinfo_t_get_base_input_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.base_input_level;
|
||||
}
|
||||
|
||||
int rocksdb_compactionservice_jobinfo_t_get_output_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.output_level;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_compactionservice_jobinfo_t_is_full_compaction(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.is_full_compaction;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_compactionservice_jobinfo_t_is_manual_compaction(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.is_manual_compaction;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_compactionservice_jobinfo_t_is_bottommost_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.bottommost_level;
|
||||
}
|
||||
|
||||
// Helper function to validate compaction service job status
|
||||
static inline bool IsValidCompactionServiceJobStatus(int status) {
|
||||
return status >= rocksdb_compactionservice_jobstatus_success &&
|
||||
status <= rocksdb_compactionservice_jobstatus_use_local;
|
||||
}
|
||||
|
||||
rocksdb_compactionservice_scheduleresponse_t*
|
||||
rocksdb_compactionservice_scheduleresponse_create(const char* scheduled_job_id,
|
||||
int status, char** errptr) {
|
||||
// Validate status is in range [success=0, failure=1, aborted=2, use_local=3]
|
||||
if (!IsValidCompactionServiceJobStatus(status)) {
|
||||
SaveError(errptr,
|
||||
Status::InvalidArgument("Invalid status value. Must be 0-3."));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
rocksdb_compactionservice_scheduleresponse_t* response =
|
||||
new rocksdb_compactionservice_scheduleresponse_t{
|
||||
CompactionServiceScheduleResponse(
|
||||
scheduled_job_id ? std::string(scheduled_job_id) : "",
|
||||
static_cast<CompactionServiceJobStatus>(status))};
|
||||
return response;
|
||||
}
|
||||
|
||||
rocksdb_compactionservice_scheduleresponse_t*
|
||||
rocksdb_compactionservice_scheduleresponse_create_with_status(int status,
|
||||
char** errptr) {
|
||||
// Validate status is in range [success=0, failure=1, aborted=2, use_local=3]
|
||||
if (!IsValidCompactionServiceJobStatus(status)) {
|
||||
SaveError(errptr,
|
||||
Status::InvalidArgument("Invalid status value. Must be 0-3."));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
rocksdb_compactionservice_scheduleresponse_t* response =
|
||||
new rocksdb_compactionservice_scheduleresponse_t{
|
||||
CompactionServiceScheduleResponse(
|
||||
static_cast<CompactionServiceJobStatus>(status))};
|
||||
return response;
|
||||
}
|
||||
|
||||
void rocksdb_compactionservice_scheduleresponse_t_destroy(
|
||||
rocksdb_compactionservice_scheduleresponse_t* response) {
|
||||
if (response) {
|
||||
delete response;
|
||||
}
|
||||
}
|
||||
|
||||
int rocksdb_compactionservice_scheduleresponse_getstatus(
|
||||
const rocksdb_compactionservice_scheduleresponse_t* response) {
|
||||
if (!response) {
|
||||
return rocksdb_compactionservice_jobstatus_failure;
|
||||
}
|
||||
return static_cast<int>(response->rep.status);
|
||||
}
|
||||
|
||||
const char* rocksdb_compactionservice_scheduleresponse_get_scheduled_job_id(
|
||||
const rocksdb_compactionservice_scheduleresponse_t* response, size_t* len) {
|
||||
if (!response || !len) {
|
||||
if (len) {
|
||||
*len = 0;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
*len = response->rep.scheduled_job_id.size();
|
||||
return response->rep.scheduled_job_id.data();
|
||||
}
|
||||
|
||||
struct rocksdb_compactionservice_t : public CompactionService {
|
||||
void* state_;
|
||||
void (*destructor_)(void*);
|
||||
rocksdb_compaction_service_schedule_cb schedule_;
|
||||
std::string name_;
|
||||
rocksdb_compaction_service_wait_cb wait_;
|
||||
rocksdb_compaction_service_cancel_awaiting_jobs_cb cancel_awaiting_jobs_;
|
||||
rocksdb_compaction_service_on_installation_cb on_installation_;
|
||||
|
||||
rocksdb_compactionservice_t(
|
||||
void* state, void (*destructor)(void*),
|
||||
rocksdb_compaction_service_schedule_cb
|
||||
rocksdb_compaction_service_schedule_ptr,
|
||||
const char* name, rocksdb_compaction_service_wait_cb wait,
|
||||
rocksdb_compaction_service_cancel_awaiting_jobs_cb cancel_awaiting_jobs,
|
||||
rocksdb_compaction_service_on_installation_cb on_installation)
|
||||
: state_(state),
|
||||
destructor_(destructor),
|
||||
schedule_(rocksdb_compaction_service_schedule_ptr),
|
||||
name_(name ? name : "CompactionService"),
|
||||
wait_(wait),
|
||||
cancel_awaiting_jobs_(cancel_awaiting_jobs),
|
||||
on_installation_(on_installation) {}
|
||||
|
||||
~rocksdb_compactionservice_t() override {
|
||||
if (destructor_) {
|
||||
(*destructor_)(state_);
|
||||
}
|
||||
}
|
||||
|
||||
const char* Name() const override { return name_.c_str(); }
|
||||
|
||||
CompactionServiceScheduleResponse Schedule(
|
||||
const CompactionServiceJobInfo& info,
|
||||
const std::string& compaction_service_input) override {
|
||||
if (schedule_ == nullptr) {
|
||||
return CompactionServiceScheduleResponse(
|
||||
CompactionServiceJobStatus::kUseLocal);
|
||||
}
|
||||
|
||||
rocksdb_compactionservice_scheduleresponse_t* c_response = (*schedule_)(
|
||||
state_,
|
||||
reinterpret_cast<const rocksdb_compactionservice_jobinfo_t*>(&info),
|
||||
compaction_service_input.data(), compaction_service_input.size());
|
||||
|
||||
if (c_response == nullptr) {
|
||||
return CompactionServiceScheduleResponse(
|
||||
CompactionServiceJobStatus::kFailure);
|
||||
}
|
||||
|
||||
CompactionServiceScheduleResponse response = std::move(c_response->rep);
|
||||
delete c_response;
|
||||
return response;
|
||||
}
|
||||
|
||||
CompactionServiceJobStatus Wait(const std::string& scheduled_job_id,
|
||||
std::string* result) override {
|
||||
if (wait_ == nullptr) {
|
||||
return CompactionServiceJobStatus::kUseLocal;
|
||||
}
|
||||
|
||||
char* c_result = nullptr;
|
||||
size_t result_len = 0;
|
||||
|
||||
int status =
|
||||
(*wait_)(state_, scheduled_job_id.c_str(), &c_result, &result_len);
|
||||
|
||||
if (c_result != nullptr) {
|
||||
if (result != nullptr) {
|
||||
result->assign(c_result, result_len);
|
||||
}
|
||||
free(c_result);
|
||||
}
|
||||
|
||||
return static_cast<CompactionServiceJobStatus>(status);
|
||||
}
|
||||
|
||||
void CancelAwaitingJobs() override {
|
||||
if (cancel_awaiting_jobs_ != nullptr) {
|
||||
(*cancel_awaiting_jobs_)(state_);
|
||||
}
|
||||
}
|
||||
|
||||
void OnInstallation(const std::string& scheduled_job_id,
|
||||
CompactionServiceJobStatus status) override {
|
||||
if (on_installation_ != nullptr) {
|
||||
(*on_installation_)(state_, scheduled_job_id.c_str(),
|
||||
static_cast<int>(status));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
rocksdb_compactionservice_t* rocksdb_compactionservice_create(
|
||||
void* state, void (*destructor)(void*),
|
||||
rocksdb_compaction_service_schedule_cb schedule, const char* name,
|
||||
rocksdb_compaction_service_wait_cb wait,
|
||||
rocksdb_compaction_service_cancel_awaiting_jobs_cb cancel_awaiting_jobs,
|
||||
rocksdb_compaction_service_on_installation_cb on_installation) {
|
||||
return new rocksdb_compactionservice_t(state, destructor, schedule, name,
|
||||
wait, cancel_awaiting_jobs,
|
||||
on_installation);
|
||||
}
|
||||
|
||||
void rocksdb_options_set_compaction_service(
|
||||
rocksdb_options_t* opt, rocksdb_compactionservice_t* service) {
|
||||
if (!opt || !service) {
|
||||
return;
|
||||
}
|
||||
|
||||
opt->rep.compaction_service = std::shared_ptr<CompactionService>(service);
|
||||
}
|
||||
|
||||
// CompactionServiceOptionsOverride functions
|
||||
rocksdb_compaction_service_options_override_t*
|
||||
rocksdb_compaction_service_options_override_create() {
|
||||
return new rocksdb_compaction_service_options_override_t;
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_destroy(
|
||||
rocksdb_compaction_service_options_override_t* override_options) {
|
||||
if (override_options) {
|
||||
delete override_options;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_env(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_env_t* env) {
|
||||
if (override_options && env) {
|
||||
override_options->rep.env = env->rep;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_comparator(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_comparator_t* comparator) {
|
||||
if (override_options && comparator) {
|
||||
override_options->rep.comparator = comparator;
|
||||
}
|
||||
}
|
||||
|
||||
// Atomic bool management for cancellation
|
||||
unsigned char* rocksdb_open_and_compact_canceled_create() {
|
||||
return reinterpret_cast<unsigned char*>(new std::atomic<bool>(false));
|
||||
}
|
||||
|
||||
void rocksdb_open_and_compact_canceled_destroy(unsigned char* canceled) {
|
||||
if (canceled) {
|
||||
delete reinterpret_cast<std::atomic<bool>*>(canceled);
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_open_and_compact_canceled_set(unsigned char* canceled,
|
||||
unsigned char value) {
|
||||
if (canceled) {
|
||||
reinterpret_cast<std::atomic<bool>*>(canceled)->store(value != 0);
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAndCompactOptions functions
|
||||
rocksdb_open_and_compact_options_t* rocksdb_open_and_compact_options_create() {
|
||||
return new rocksdb_open_and_compact_options_t;
|
||||
}
|
||||
|
||||
void rocksdb_open_and_compact_options_destroy(
|
||||
rocksdb_open_and_compact_options_t* options) {
|
||||
if (options) {
|
||||
delete options;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_open_and_compact_options_set_canceled(
|
||||
rocksdb_open_and_compact_options_t* options, unsigned char* canceled) {
|
||||
if (options && canceled) {
|
||||
options->rep.canceled = reinterpret_cast<std::atomic<bool>*>(canceled);
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_open_and_compact_options_set_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* options,
|
||||
unsigned char allow_resumption) {
|
||||
if (options) {
|
||||
options->rep.allow_resumption = allow_resumption != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAndCompact functions
|
||||
char* rocksdb_open_and_compact(
|
||||
const char* db_path, const char* output_directory, const char* input,
|
||||
size_t input_len, size_t* output_len,
|
||||
const rocksdb_compaction_service_options_override_t* override_options,
|
||||
char** errptr) {
|
||||
if (!db_path || !output_directory || !input || !override_options) {
|
||||
SaveError(errptr, Status::InvalidArgument("Invalid arguments"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string input_str(input, input_len);
|
||||
std::string output_str;
|
||||
|
||||
Status s = DB::OpenAndCompact(db_path, output_directory, input_str,
|
||||
&output_str, override_options->rep);
|
||||
|
||||
if (!s.ok()) {
|
||||
SaveError(errptr, s);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Allocate +1 for null terminator
|
||||
char* result = static_cast<char*>(malloc(output_str.size() + 1));
|
||||
if (!result) {
|
||||
SaveError(errptr, Status::MemoryLimit("Failed to allocate output buffer"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
memcpy(result, output_str.data(), output_str.size());
|
||||
result[output_str.size()] = '\0';
|
||||
|
||||
// Only set output_len after successful allocation
|
||||
if (output_len) {
|
||||
*output_len = output_str.size();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
char* rocksdb_open_and_compact_with_options(
|
||||
const rocksdb_open_and_compact_options_t* options, const char* db_path,
|
||||
const char* output_directory, const char* input, size_t input_len,
|
||||
size_t* output_len,
|
||||
const rocksdb_compaction_service_options_override_t* override_options,
|
||||
char** errptr) {
|
||||
if (!options || !db_path || !output_directory || !input ||
|
||||
!override_options) {
|
||||
SaveError(errptr, Status::InvalidArgument("Invalid arguments"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string input_str(input, input_len);
|
||||
std::string output_str;
|
||||
|
||||
Status s = DB::OpenAndCompact(options->rep, db_path, output_directory,
|
||||
input_str, &output_str, override_options->rep);
|
||||
|
||||
if (!s.ok()) {
|
||||
SaveError(errptr, s);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Allocate +1 for null terminator
|
||||
char* result = static_cast<char*>(malloc(output_str.size() + 1));
|
||||
if (!result) {
|
||||
SaveError(errptr, Status::MemoryLimit("Failed to allocate output buffer"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
memcpy(result, output_str.data(), output_str.size());
|
||||
result[output_str.size()] = '\0'; // Null terminate
|
||||
|
||||
// Only set output_len after successful allocation
|
||||
if (output_len) {
|
||||
*output_len = output_str.size();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
rocksdb_t* rocksdb_open(const rocksdb_options_t* options, const char* name,
|
||||
char** errptr) {
|
||||
DB* db;
|
||||
@@ -7841,7 +8267,7 @@ rocksdb_memory_usage_t* rocksdb_approximate_memory_usage_create(
|
||||
dbs.push_back(db->rep);
|
||||
}
|
||||
|
||||
unordered_set<const Cache*> cache_set;
|
||||
std::unordered_set<const Cache*> cache_set;
|
||||
for (auto cache : consumers->caches) {
|
||||
cache_set.insert(const_cast<const Cache*>(cache->rep.get()));
|
||||
}
|
||||
|
||||
+295
@@ -722,6 +722,88 @@ static void LoadAndCheckLatestOptions(const char* db_name, rocksdb_env_t* env,
|
||||
num_column_families);
|
||||
}
|
||||
|
||||
// Global state for tracking remote compaction calls
|
||||
typedef struct {
|
||||
int schedule_called;
|
||||
int wait_called;
|
||||
int cancel_called;
|
||||
char last_scheduled_job_id[256];
|
||||
char last_db_name[256];
|
||||
} RemoteCompactionState;
|
||||
|
||||
// Schedule callback - gets called when compaction is scheduled
|
||||
static rocksdb_compactionservice_scheduleresponse_t* RemoteCompactionSchedule(
|
||||
void* state, const rocksdb_compactionservice_jobinfo_t* info,
|
||||
const char* input, size_t input_len) {
|
||||
(void)input;
|
||||
(void)input_len;
|
||||
RemoteCompactionState* rcs = (RemoteCompactionState*)state;
|
||||
rcs->schedule_called++;
|
||||
|
||||
// Extract job info
|
||||
size_t db_name_len;
|
||||
const char* db_name =
|
||||
rocksdb_compactionservice_jobinfo_t_get_db_name(info, &db_name_len);
|
||||
memcpy(rcs->last_db_name, db_name, db_name_len);
|
||||
rcs->last_db_name[db_name_len] = '\0';
|
||||
|
||||
// Generate a job ID
|
||||
snprintf(rcs->last_scheduled_job_id, sizeof(rcs->last_scheduled_job_id),
|
||||
"job-%d", rcs->schedule_called);
|
||||
|
||||
// Create response with success status
|
||||
char* err = NULL;
|
||||
rocksdb_compactionservice_scheduleresponse_t* response =
|
||||
rocksdb_compactionservice_scheduleresponse_create(
|
||||
rcs->last_scheduled_job_id,
|
||||
rocksdb_compactionservice_jobstatus_success, &err);
|
||||
if (err) {
|
||||
free(err);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Wait callback - simulates waiting for remote compaction to complete
|
||||
static int RemoteCompactionWait(void* state, const char* scheduled_job_id,
|
||||
char** result, size_t* result_len) {
|
||||
RemoteCompactionState* rcs = (RemoteCompactionState*)state;
|
||||
rcs->wait_called++;
|
||||
|
||||
if (strcmp(scheduled_job_id, rcs->last_scheduled_job_id) != 0) {
|
||||
return rocksdb_compactionservice_jobstatus_failure;
|
||||
}
|
||||
|
||||
// For testing purposes, return kUseLocal to cause RocksDB to fall back to
|
||||
// local compaction. This tests the callback mechanism without needing a fully
|
||||
// serialized result. In a real scenario, this would communicate with a remote
|
||||
// worker that calls rocksdb_open_and_compact() and returns a properly
|
||||
// serialized CompactionServiceResult
|
||||
*result = NULL;
|
||||
*result_len = 0;
|
||||
|
||||
return rocksdb_compactionservice_jobstatus_use_local;
|
||||
}
|
||||
|
||||
// Cancel callback - cancels pending jobs
|
||||
static void RemoteCompactionCancel(void* state) {
|
||||
RemoteCompactionState* rcs = (RemoteCompactionState*)state;
|
||||
rcs->cancel_called++;
|
||||
}
|
||||
|
||||
// Destructor callback
|
||||
static void RemoteCompactionDestroy(void* state) { (void)state; }
|
||||
|
||||
// NULL schedule callback for testing failure handling
|
||||
static rocksdb_compactionservice_scheduleresponse_t* NullSchedule(
|
||||
void* state, const rocksdb_compactionservice_jobinfo_t* info,
|
||||
const char* input, size_t input_len) {
|
||||
(void)state;
|
||||
(void)info;
|
||||
(void)input;
|
||||
(void)input_len;
|
||||
return NULL; // Return NULL to simulate failure
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
@@ -4483,6 +4565,219 @@ int main(int argc, char** argv) {
|
||||
rocksdb_cache_destroy(lru);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_service");
|
||||
{
|
||||
RemoteCompactionState remote_state = {0, 0, 0, "", ""};
|
||||
|
||||
// Create compaction service
|
||||
rocksdb_compactionservice_t* service = rocksdb_compactionservice_create(
|
||||
&remote_state, // state
|
||||
RemoteCompactionDestroy, // destructor
|
||||
RemoteCompactionSchedule, // schedule callback
|
||||
"TestRemoteCompaction", // name
|
||||
RemoteCompactionWait, // wait callback
|
||||
RemoteCompactionCancel, // cancel_awaiting_jobs
|
||||
NULL); // on_installation
|
||||
|
||||
// Create options with remote compaction
|
||||
rocksdb_options_t* remote_options = rocksdb_options_create();
|
||||
rocksdb_options_set_create_if_missing(remote_options, 1);
|
||||
rocksdb_options_set_level0_file_num_compaction_trigger(remote_options, 2);
|
||||
rocksdb_options_set_write_buffer_size(remote_options,
|
||||
64 * 1024); // 64KB buffer
|
||||
rocksdb_options_set_max_bytes_for_level_base(remote_options,
|
||||
256 * 1024); // 256KB
|
||||
rocksdb_options_set_target_file_size_base(
|
||||
remote_options, 64 * 1024); // 64KB target file size
|
||||
// Disable automatic compactions to test manual compaction only
|
||||
rocksdb_options_set_disable_auto_compactions(remote_options, 1);
|
||||
rocksdb_options_set_compaction_service(remote_options, service);
|
||||
|
||||
// Destroy old DB and create new one
|
||||
rocksdb_close(db);
|
||||
rocksdb_destroy_db(remote_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
db = rocksdb_open(remote_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// Create multiple SST files to trigger compaction
|
||||
rocksdb_flushoptions_t* flush_opts = rocksdb_flushoptions_create();
|
||||
rocksdb_flushoptions_set_wait(flush_opts, 1);
|
||||
|
||||
// Write and flush multiple times to create multiple L0 files
|
||||
// Write more data with larger values to ensure files are substantial
|
||||
for (int batch = 0; batch < 5; batch++) {
|
||||
for (int i = 0; i < 200; i++) {
|
||||
char key[20], val[1000];
|
||||
snprintf(key, sizeof(key), "key%d_%d", batch, i);
|
||||
// Fill value with repeated data to make it larger
|
||||
memset(val, 'a' + (batch % 26), sizeof(val) - 1);
|
||||
val[sizeof(val) - 1] = '\0';
|
||||
rocksdb_put(db, woptions, key, strlen(key), val, strlen(val), &err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
rocksdb_flush(db, flush_opts, &err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
rocksdb_flushoptions_destroy(flush_opts);
|
||||
|
||||
// Trigger manual compaction to invoke remote compaction service
|
||||
rocksdb_compact_range(db, NULL, 0, NULL, 0);
|
||||
|
||||
rocksdb_wait_for_compact_options_t* wco =
|
||||
rocksdb_wait_for_compact_options_create();
|
||||
rocksdb_wait_for_compact(db, wco, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_wait_for_compact_options_destroy(wco);
|
||||
|
||||
// Verify that callbacks were actually called
|
||||
CheckCondition(remote_state.schedule_called > 0);
|
||||
CheckCondition(remote_state.wait_called > 0);
|
||||
CheckCondition(strlen(remote_state.last_db_name) > 0);
|
||||
CheckCondition(strstr(remote_state.last_db_name, "rocksdb_c_test") != NULL);
|
||||
|
||||
// Verify data is still accessible after remote compaction
|
||||
// Just check a few keys to verify data integrity
|
||||
for (int batch = 0; batch < 5; batch++) {
|
||||
char key[20];
|
||||
snprintf(key, sizeof(key), "key%d_0", batch);
|
||||
size_t vallen;
|
||||
char* val = rocksdb_get(db, roptions, key, strlen(key), &vallen, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(val != NULL);
|
||||
CheckCondition(vallen == 999); // strlen of 1000-byte string
|
||||
free(val);
|
||||
}
|
||||
|
||||
// Test cancellation API directly
|
||||
RemoteCompactionCancel(&remote_state);
|
||||
CheckCondition(remote_state.cancel_called > 0);
|
||||
|
||||
// Cleanup
|
||||
rocksdb_close(db);
|
||||
rocksdb_destroy_db(remote_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_options_destroy(remote_options);
|
||||
|
||||
// Reopen DB with original options for subsequent tests
|
||||
db = rocksdb_open(options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_scheduleresponse");
|
||||
{
|
||||
// Test scheduleresponse creation and getters
|
||||
rocksdb_compactionservice_scheduleresponse_t* response;
|
||||
|
||||
// Test success response
|
||||
err = NULL;
|
||||
response = rocksdb_compactionservice_scheduleresponse_create(
|
||||
"test-job-123", rocksdb_compactionservice_jobstatus_success, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(response != NULL);
|
||||
CheckCondition(
|
||||
rocksdb_compactionservice_scheduleresponse_getstatus(response) ==
|
||||
rocksdb_compactionservice_jobstatus_success);
|
||||
|
||||
size_t job_id_len;
|
||||
const char* job_id =
|
||||
rocksdb_compactionservice_scheduleresponse_get_scheduled_job_id(
|
||||
response, &job_id_len);
|
||||
CheckCondition(job_id_len == strlen("test-job-123"));
|
||||
CheckCondition(memcmp(job_id, "test-job-123", job_id_len) == 0);
|
||||
rocksdb_compactionservice_scheduleresponse_t_destroy(response);
|
||||
|
||||
// Test failure response
|
||||
response = rocksdb_compactionservice_scheduleresponse_create_with_status(
|
||||
rocksdb_compactionservice_jobstatus_failure, &err);
|
||||
CheckCondition(response != NULL);
|
||||
CheckCondition(
|
||||
rocksdb_compactionservice_scheduleresponse_getstatus(response) ==
|
||||
rocksdb_compactionservice_jobstatus_failure);
|
||||
rocksdb_compactionservice_scheduleresponse_t_destroy(response);
|
||||
|
||||
response = rocksdb_compactionservice_scheduleresponse_create_with_status(
|
||||
999, &err);
|
||||
CheckCondition(response == NULL); // Invalid status
|
||||
if (err) {
|
||||
Free(&err);
|
||||
}
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_options_override");
|
||||
{
|
||||
// Test CompactionServiceOptionsOverride API
|
||||
rocksdb_compaction_service_options_override_t* override_opts =
|
||||
rocksdb_compaction_service_options_override_create();
|
||||
CheckCondition(override_opts != NULL);
|
||||
|
||||
// Set up override options
|
||||
rocksdb_compaction_service_options_override_set_env(override_opts, env);
|
||||
rocksdb_compaction_service_options_override_set_comparator(override_opts,
|
||||
cmp);
|
||||
|
||||
rocksdb_compaction_service_options_override_destroy(override_opts);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_null_callback_handling");
|
||||
{
|
||||
// Test that NULL callback returns are handled gracefully
|
||||
// This simulates a failure in the remote compaction service
|
||||
rocksdb_compactionservice_t* null_service =
|
||||
rocksdb_compactionservice_create(NULL, NULL, NullSchedule,
|
||||
"NullTestService", NULL, NULL, NULL);
|
||||
|
||||
rocksdb_options_t* null_opts = rocksdb_options_create();
|
||||
rocksdb_options_set_create_if_missing(null_opts, 1);
|
||||
rocksdb_options_set_compaction_service(null_opts, null_service);
|
||||
|
||||
const char* null_db = "rocksdb_c_test_null_service";
|
||||
|
||||
rocksdb_t* null_db_handle = rocksdb_open(null_opts, null_db, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// Write data and trigger compaction
|
||||
for (int i = 0; i < 100; i++) {
|
||||
char key[20], val[50];
|
||||
snprintf(key, sizeof(key), "key%d", i);
|
||||
snprintf(val, sizeof(val), "val%d", i);
|
||||
rocksdb_put(null_db_handle, woptions, key, strlen(key), val, strlen(val),
|
||||
&err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
|
||||
// This should fall back to local compaction (not crash)
|
||||
rocksdb_compact_range(null_db_handle, NULL, 0, NULL, 0);
|
||||
|
||||
// Data should still be readable
|
||||
CheckGet(null_db_handle, roptions, "key50", "val50");
|
||||
|
||||
rocksdb_close(null_db_handle);
|
||||
rocksdb_destroy_db(null_opts, null_db, &err);
|
||||
rocksdb_options_destroy(null_opts);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_canceled_flag");
|
||||
{
|
||||
// Test atomic cancellation flag API
|
||||
unsigned char* canceled = rocksdb_open_and_compact_canceled_create();
|
||||
CheckCondition(canceled != NULL);
|
||||
|
||||
// Set cancellation
|
||||
rocksdb_open_and_compact_canceled_set(canceled, 1);
|
||||
|
||||
// Use with OpenAndCompactOptions
|
||||
rocksdb_open_and_compact_options_t* oac_opts =
|
||||
rocksdb_open_and_compact_options_create();
|
||||
rocksdb_open_and_compact_options_set_canceled(oac_opts, canceled);
|
||||
rocksdb_open_and_compact_options_set_allow_resumption(oac_opts, 1);
|
||||
|
||||
// Cleanup
|
||||
rocksdb_open_and_compact_options_destroy(oac_opts);
|
||||
rocksdb_open_and_compact_canceled_destroy(canceled);
|
||||
}
|
||||
|
||||
StartPhase("sst_file_manager");
|
||||
{
|
||||
rocksdb_sst_file_manager_t* sst_file_manager;
|
||||
|
||||
@@ -1480,11 +1480,11 @@ CompactionJob::CreateFileHandlers(SubcompactionState* sub_compact,
|
||||
const CompactionFileCloseFunc close_file_func =
|
||||
[this, sub_compact, start_user_key, end_user_key](
|
||||
const Status& status,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_key, const CompactionIterator* c_iter,
|
||||
CompactionOutputs& outputs) {
|
||||
return this->FinishCompactionOutputFile(
|
||||
status, prev_table_last_internal_key, next_table_min_key,
|
||||
status, prev_iter_output_internal_key, next_table_min_key,
|
||||
start_user_key, end_user_key, c_iter, sub_compact, outputs);
|
||||
};
|
||||
|
||||
@@ -1499,8 +1499,8 @@ Status CompactionJob::ProcessKeyValue(
|
||||
const uint64_t kRecordStatsEvery = 1000;
|
||||
[[maybe_unused]] const std::optional<const Slice> end = sub_compact->end;
|
||||
|
||||
IterKey last_output_key;
|
||||
ParsedInternalKey last_output_ikey;
|
||||
IterKey prev_iter_output_key;
|
||||
ParsedInternalKey prev_iter_output_internal_key;
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"CompactionJob::ProcessKeyValueCompaction()::Processing",
|
||||
@@ -1551,9 +1551,9 @@ Status CompactionJob::ProcessKeyValue(
|
||||
// and `close_file_func`.
|
||||
// TODO: it would be better to have the compaction file open/close moved
|
||||
// into `CompactionOutputs` which has the output file information.
|
||||
status =
|
||||
sub_compact->AddToOutput(*c_iter, use_proximal_output, open_file_func,
|
||||
close_file_func, last_output_ikey);
|
||||
status = sub_compact->AddToOutput(*c_iter, use_proximal_output,
|
||||
open_file_func, close_file_func,
|
||||
prev_iter_output_internal_key);
|
||||
if (!status.ok()) {
|
||||
break;
|
||||
}
|
||||
@@ -1562,9 +1562,10 @@ Status CompactionJob::ProcessKeyValue(
|
||||
static_cast<void*>(const_cast<std::atomic<bool>*>(
|
||||
&manual_compaction_canceled_)));
|
||||
|
||||
last_output_key.SetInternalKey(c_iter->key(), &last_output_ikey);
|
||||
last_output_ikey.sequence = ikey.sequence;
|
||||
last_output_ikey.type = ikey.type;
|
||||
prev_iter_output_key.SetInternalKey(c_iter->key(),
|
||||
&prev_iter_output_internal_key);
|
||||
prev_iter_output_internal_key.sequence = ikey.sequence;
|
||||
prev_iter_output_internal_key.type = ikey.type;
|
||||
c_iter->Next();
|
||||
|
||||
#ifndef NDEBUG
|
||||
@@ -1871,7 +1872,7 @@ void CompactionJob::RecordDroppedKeys(
|
||||
|
||||
Status CompactionJob::FinishCompactionOutputFile(
|
||||
const Status& input_status,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_key, const Slice* comp_start_user_key,
|
||||
const Slice* comp_end_user_key, const CompactionIterator* c_iter,
|
||||
SubcompactionState* sub_compact, CompactionOutputs& outputs) {
|
||||
@@ -2049,7 +2050,7 @@ Status CompactionJob::FinishCompactionOutputFile(
|
||||
}
|
||||
|
||||
if (s.ok() && ShouldUpdateSubcompactionProgress(sub_compact, c_iter,
|
||||
prev_table_last_internal_key,
|
||||
prev_iter_output_internal_key,
|
||||
next_table_min_key, meta)) {
|
||||
UpdateSubcompactionProgress(c_iter, next_table_min_key, sub_compact);
|
||||
s = PersistSubcompactionProgress(sub_compact);
|
||||
@@ -2060,10 +2061,10 @@ Status CompactionJob::FinishCompactionOutputFile(
|
||||
|
||||
bool CompactionJob::ShouldUpdateSubcompactionProgress(
|
||||
const SubcompactionState* sub_compact, const CompactionIterator* c_iter,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_internal_key, const FileMetaData* meta) const {
|
||||
const auto* cfd = sub_compact->compaction->column_family_data();
|
||||
// No need to update when the output will not get persisted
|
||||
// No need to update when the progress will not get persisted
|
||||
if (compaction_progress_writer_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -2087,19 +2088,24 @@ bool CompactionJob::ShouldUpdateSubcompactionProgress(
|
||||
}
|
||||
|
||||
// LIMITATION: Compaction progress persistence disabled for file boundaries
|
||||
// contaning range deletions. Range deletions can span file boundaries, making
|
||||
// it difficult (but possible) to ensure adjacent output tables have different
|
||||
// user keys. See the last check for why different users keys of adjacent
|
||||
// output tables are needed
|
||||
// containing range deletions. Range deletions can span file boundaries,
|
||||
// making it difficult to ensure adjacent output tables have different user
|
||||
// keys. See the last check for why different users keys of adjacent output
|
||||
// tables are needed
|
||||
const ValueType next_table_min_internal_key_type =
|
||||
ExtractValueType(next_table_min_internal_key);
|
||||
const ValueType prev_table_last_internal_key_type =
|
||||
prev_table_last_internal_key.user_key.empty()
|
||||
const ValueType prev_iter_output_internal_key_type =
|
||||
prev_iter_output_internal_key.user_key.empty()
|
||||
? ValueType::kTypeValue
|
||||
: prev_table_last_internal_key.type;
|
||||
: prev_iter_output_internal_key.type;
|
||||
|
||||
if (next_table_min_internal_key_type == ValueType::kTypeRangeDeletion ||
|
||||
prev_table_last_internal_key_type == ValueType::kTypeRangeDeletion) {
|
||||
// Range deletes truncated to align with file boundaries may be output by the
|
||||
// compaction iterator with `ValueType::kTypeMaxValid` instead of the original
|
||||
// type.
|
||||
if ((next_table_min_internal_key_type == ValueType::kTypeRangeDeletion ||
|
||||
next_table_min_internal_key_type == ValueType::kTypeMaxValid) ||
|
||||
(prev_iter_output_internal_key_type == ValueType::kTypeRangeDeletion ||
|
||||
prev_iter_output_internal_key_type == ValueType::kTypeMaxValid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2109,9 +2115,9 @@ bool CompactionJob::ShouldUpdateSubcompactionProgress(
|
||||
const Slice next_table_min_user_key =
|
||||
ExtractUserKey(next_table_min_internal_key);
|
||||
const Slice prev_table_last_user_key =
|
||||
prev_table_last_internal_key.user_key.empty()
|
||||
prev_iter_output_internal_key.user_key.empty()
|
||||
? Slice()
|
||||
: prev_table_last_internal_key.user_key;
|
||||
: prev_iter_output_internal_key.user_key;
|
||||
|
||||
if (cfd->user_comparator()->EqualWithoutTimestamp(next_table_min_user_key,
|
||||
prev_table_last_user_key)) {
|
||||
|
||||
@@ -430,7 +430,7 @@ class CompactionJob {
|
||||
|
||||
Status FinishCompactionOutputFile(
|
||||
const Status& input_status,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_key, const Slice* comp_start_user_key,
|
||||
const Slice* comp_end_user_key, const CompactionIterator* c_iter,
|
||||
SubcompactionState* sub_compact, CompactionOutputs& outputs);
|
||||
@@ -545,7 +545,7 @@ class CompactionJob {
|
||||
|
||||
bool ShouldUpdateSubcompactionProgress(
|
||||
const SubcompactionState* sub_compact, const CompactionIterator* c_iter,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_internal_key, const FileMetaData* meta) const;
|
||||
|
||||
void UpdateSubcompactionProgress(const CompactionIterator* c_iter,
|
||||
|
||||
@@ -2424,6 +2424,7 @@ class ResumableCompactionJobTest : public CompactionJobTestBase {
|
||||
bool enable_cancel_ = false;
|
||||
std::atomic<int> stop_count_{0};
|
||||
std::atomic<bool> cancel_{false};
|
||||
SequenceNumber cancel_before_seqno = kMaxSequenceNumber;
|
||||
|
||||
void SetUp() override {
|
||||
CompactionJobTestBase::SetUp();
|
||||
@@ -2437,7 +2438,9 @@ class ResumableCompactionJobTest : public CompactionJobTestBase {
|
||||
if (enable_cancel_) {
|
||||
ParsedInternalKey parsed_key;
|
||||
if (ParseInternalKey(pair->second, &parsed_key, true).ok()) {
|
||||
if (parsed_key.user_key == kCancelBeforeThisKey) {
|
||||
if (parsed_key.user_key == kCancelBeforeThisKey &&
|
||||
(cancel_before_seqno == kMaxSequenceNumber ||
|
||||
parsed_key.sequence == cancel_before_seqno)) {
|
||||
cancel_.store(true);
|
||||
}
|
||||
}
|
||||
@@ -2665,7 +2668,7 @@ class ResumableCompactionJobTest : public CompactionJobTestBase {
|
||||
const std::initializer_list<mock::KVPair>& input_file_2,
|
||||
uint64_t last_sequence, const std::vector<uint64_t>& snapshots,
|
||||
const std::string& expected_next_key_to_compact,
|
||||
const std::vector<std::string>& expected_input_keys, bool exists_progress,
|
||||
const std::vector<std::string>& expected_input_keys,
|
||||
bool cancelled_past_mid_point = false) {
|
||||
std::shared_ptr<Statistics> stats = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
|
||||
@@ -2701,7 +2704,7 @@ class ResumableCompactionJobTest : public CompactionJobTestBase {
|
||||
|
||||
// Resume compaction
|
||||
CompactionProgress compaction_progress;
|
||||
if (exists_progress) {
|
||||
if (expected_next_key_to_compact != "") {
|
||||
compaction_progress.push_back(
|
||||
ReadAndParseProgress(compaction_progress_file));
|
||||
}
|
||||
@@ -2774,35 +2777,43 @@ TEST_F(ResumableCompactionJobTest, BasicProgressResume) {
|
||||
4U /* last_sequence */, {} /* snapshots */,
|
||||
kCancelBeforeThisKey /* expected_next_key_to_compact */,
|
||||
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
true /* exists_progress */, true /* cancelled_past_mid_point*/);
|
||||
true /* cancelled_past_mid_point */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSameKey) {
|
||||
NewDB();
|
||||
|
||||
// `cancel_before_seqno` is set to 0U to force cancellation after
|
||||
// `kCancelBeforeThisKey@1` instead of `kCancelBeforeThisKey@2`.
|
||||
// The seqno is 0 because `kCancelBeforeThisKey@1` will have its sequence
|
||||
// number zeroed during compaction while `kCancelBeforeThisKey@2` won't be
|
||||
cancel_before_seqno = 0U;
|
||||
RunCancelAndResumeTest(
|
||||
{{KeyStr(kCancelBeforeThisKey, 1U, kTypeValue),
|
||||
"val1"}} /* input_file_1 */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 2U, kTypeValue),
|
||||
"val2"}} /* input_file_2 */,
|
||||
2U /* last_sequence */, {1U} /* snapshots */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 2U, kTypeValue), "val11"},
|
||||
{KeyStr("d", 3U, kTypeValue), "val2"}} /* input_file_2 */,
|
||||
3U /* last_sequence */, {1U} /* snapshots */,
|
||||
"" /* expected_next_key_to_compact */,
|
||||
{kCancelBeforeThisKey, kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
false /* exists_progress */);
|
||||
{kCancelBeforeThisKey, kCancelBeforeThisKey,
|
||||
"d"} /* expected_input_keys */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeleteRange) {
|
||||
NewDB();
|
||||
|
||||
RunCancelAndResumeTest(
|
||||
{{KeyStr(kCancelBeforeThisKey, 1U, kTypeValue),
|
||||
"val1"}} /* input_file_1 */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 2U, kTypeRangeDeletion),
|
||||
"val2"}} /* input_file_2 */,
|
||||
2U /* last_sequence */, {1U} /* snapshots */,
|
||||
"" /* expected_next_key_to_compact */,
|
||||
{kCancelBeforeThisKey, kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
false /* exists_progress */);
|
||||
{{KeyStr("a", 1U, kTypeValue), "val1"},
|
||||
{KeyStr("b", 2U, kTypeValue), "val2"},
|
||||
{KeyStr(kCancelBeforeThisKey, 3U, kTypeValue),
|
||||
"val3"}} /* input_file_1 */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 4U, kTypeRangeDeletion),
|
||||
"range_deletion_end_key"},
|
||||
{KeyStr("d", 5U, kTypeValue), "val4"}} /* input_file_2 */,
|
||||
5U /* last_sequence */, {3U} /* snapshots */,
|
||||
"b" /* expected_next_key_to_compact */,
|
||||
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
|
||||
"d"} /* expected_input_keys */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnMerge) {
|
||||
@@ -2817,8 +2828,7 @@ TEST_F(ResumableCompactionJobTest, NoProgressResumeOnMerge) {
|
||||
"val4"}} /* input_file_2 */,
|
||||
4U /* last_sequence */, {} /* snapshots */,
|
||||
"bb" /* expected_next_key_to_compact */,
|
||||
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
true /* exists_progress */);
|
||||
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSingleDelete) {
|
||||
@@ -2834,8 +2844,7 @@ TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSingleDelete) {
|
||||
5U /* last_sequence */, {3U} /* snapshots */,
|
||||
"b" /* expected_next_key_to_compact */,
|
||||
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
|
||||
"d"} /* expected_input_keys */,
|
||||
true /* exists_progress */);
|
||||
"d"} /* expected_input_keys */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeletionAtBottom) {
|
||||
@@ -2851,8 +2860,7 @@ TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeletionAtBottom) {
|
||||
5U /* last_sequence */, {3U} /* snapshots */,
|
||||
"b" /* expected_next_key_to_compact */,
|
||||
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
|
||||
"d"} /* expected_input_keys */,
|
||||
true /* exists_progress */);
|
||||
"d"} /* expected_input_keys */);
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
|
||||
@@ -364,7 +364,7 @@ Status CompactionOutputs::AddToOutput(
|
||||
const CompactionIterator& c_iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key) {
|
||||
const ParsedInternalKey& prev_iter_output_internal_key) {
|
||||
Status s;
|
||||
bool is_range_del = c_iter.IsDeleteRangeSentinelKey();
|
||||
if (is_range_del && compaction_->bottommost_level()) {
|
||||
@@ -375,8 +375,8 @@ Status CompactionOutputs::AddToOutput(
|
||||
}
|
||||
const Slice& key = c_iter.key();
|
||||
if (ShouldStopBefore(c_iter) && HasBuilder()) {
|
||||
s = close_file_func(c_iter.InputStatus(), prev_table_last_internal_key, key,
|
||||
&c_iter, *this);
|
||||
s = close_file_func(c_iter.InputStatus(), prev_iter_output_internal_key,
|
||||
key, &c_iter, *this);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ class CompactionOutputs {
|
||||
Status AddToOutput(const CompactionIterator& c_iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key);
|
||||
const ParsedInternalKey& prev_iter_output_internal_key);
|
||||
|
||||
// Close the current output. `open_file_func` is needed for creating new file
|
||||
// for range-dels only output file.
|
||||
|
||||
@@ -109,12 +109,12 @@ Status SubcompactionState::AddToOutput(
|
||||
const CompactionIterator& iter, bool use_proximal_output,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key) {
|
||||
const ParsedInternalKey& prev_iter_output_internal_key) {
|
||||
// update target output
|
||||
current_outputs_ =
|
||||
use_proximal_output ? &proximal_level_outputs_ : &compaction_outputs_;
|
||||
return current_outputs_->AddToOutput(iter, open_file_func, close_file_func,
|
||||
prev_table_last_internal_key);
|
||||
prev_iter_output_internal_key);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -221,7 +221,7 @@ class SubcompactionState {
|
||||
Status AddToOutput(const CompactionIterator& iter, bool use_proximal_output,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key);
|
||||
const ParsedInternalKey& prev_iter_output_internal_key);
|
||||
|
||||
// Close all compaction output files, both output_to_proximal_level outputs
|
||||
// and normal outputs.
|
||||
|
||||
@@ -3868,6 +3868,75 @@ TEST_F(DBBasicTest, SkipWALIfMissingTableFiles) {
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, BestEffortRecoveryFailureWithTableCacheUseAfterFree) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.env = env_;
|
||||
// Force multiple manifest files
|
||||
options.max_manifest_file_size = 1;
|
||||
options.max_manifest_space_amp_pct = 0;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Disable file deletions to preserve old manifest files for
|
||||
// best-efforts recovery to succeed
|
||||
ASSERT_OK(db_->DisableFileDeletions());
|
||||
|
||||
// Create multiple SST files to populate TableCache during
|
||||
// best-efforts recovery
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put("key" + std::to_string(i),
|
||||
std::string(1000, static_cast<char>('a' + i))));
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
// Verify we have multiple manifest files
|
||||
std::vector<std::string> files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
int manifest_count = 0;
|
||||
for (const auto& file : files) {
|
||||
if (file.find("MANIFEST") != std::string::npos) {
|
||||
manifest_count++;
|
||||
}
|
||||
}
|
||||
ASSERT_GE(manifest_count, 2);
|
||||
|
||||
// Inject corruption after TableCache is populated (count > 3), but only once
|
||||
// (injected flag) to allow best-effort recovery to trigger retry and succeed.
|
||||
// This coerce the bug: first recovery caches SSTs with reference to column
|
||||
// family's options in table cache and retry deletes column family so the
|
||||
// reference becomes dangling.
|
||||
int count = 0;
|
||||
bool injected = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionBuilder::CheckConsistencyBeforeReturn", [&](void* arg) {
|
||||
count++;
|
||||
if (count > 3 && !injected) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
*(static_cast<Status*>(arg)) =
|
||||
Status::Corruption("Injected corruption");
|
||||
injected = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
options.best_efforts_recovery = true;
|
||||
|
||||
Status s = TryReopen(options);
|
||||
ASSERT_OK(s);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
std::string value;
|
||||
// Without the fix, ASAN detects use-after-free when accessing cached SST
|
||||
// files that hold dangling references to deleted ioptions.
|
||||
s = db_->Get(ReadOptions(), "key" + std::to_string(i), &value);
|
||||
ASSERT_TRUE(s.ok() || s.IsNotFound());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, DisableTrackWal) {
|
||||
// If WAL tracking was enabled, and then disabled during reopen,
|
||||
// the previously tracked WALs should be removed from MANIFEST.
|
||||
|
||||
@@ -1102,11 +1102,6 @@ Status DBImplSecondary::InitializeCompactionWorkspace(
|
||||
return s;
|
||||
}
|
||||
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Initialized compaction workspace with %zu subcompaction "
|
||||
"progress to resume",
|
||||
compaction_progress_.size());
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -1219,6 +1214,11 @@ Status DBImplSecondary::PrepareCompactionProgressState() {
|
||||
return HandleInvalidOrNoCompactionProgress(compaction_progress_file_path,
|
||||
scan_result);
|
||||
}
|
||||
|
||||
ROCKS_LOG_DEBUG(
|
||||
immutable_db_options_.info_log,
|
||||
"Loaded compaction progress with %zu subcompaction(s) from %s",
|
||||
compaction_progress_.size(), compaction_progress_file_path.c_str());
|
||||
return s;
|
||||
} else {
|
||||
return HandleInvalidOrNoCompactionProgress(
|
||||
@@ -1740,6 +1740,11 @@ Status DBImplSecondary::FinalizeCompactionProgressWriter(
|
||||
return HandleCompactionProgressWriterCreationFailure(
|
||||
"" /* temp_file_path */, final_file_path, compaction_progress_writer);
|
||||
}
|
||||
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"Finalized compaction progress writer onto %s",
|
||||
final_file_path.c_str());
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+73
-118
@@ -3826,133 +3826,88 @@ TEST_F(DBRangeDelTest, RowCache) {
|
||||
ASSERT_OK(Put(Key(5), "foo"));
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, FileCutWithTruncatedRangeDelKey) {
|
||||
// Test for a bug that used to generate files with meta.smallest
|
||||
// containing kMaxValid.
|
||||
//
|
||||
// Setup:
|
||||
// - Write Key(2), Key(3) and DeleteRange(Key(1), Key(4))
|
||||
// - Flush to L0
|
||||
// - Use SingleKeySstPartitioner to force each user key into its own file
|
||||
// - Compact files from L0 to L1 will generate files
|
||||
// File[0]:
|
||||
// smallest=[user_key=key000001, seq=4, type=15],
|
||||
// largest= [user_key=key000002, seq=72057594037927935, type=15]
|
||||
// File[1]:
|
||||
// smallest=[user_key=key000002, seq=2, type=1],
|
||||
// largest= [user_key=key000003, seq=72057594037927935, type=15]
|
||||
// File[2]:
|
||||
// smallest=[user_key=key000003, seq=3, type=1],
|
||||
// largest= [user_key=key000004, seq=72057594037927935, type=15]
|
||||
// With range deletions truncated to each files key range.
|
||||
//
|
||||
// - Compact these files again into L2. RocksDB usede to set truncated
|
||||
// range deletion start key to have value type kMaxValid. The range deletion
|
||||
// start key is used in compaction file cutting decision.
|
||||
// - Verify the file boundary keys after compaction have valid boundary keys
|
||||
//
|
||||
// Before the fix:
|
||||
// File[0]:
|
||||
// smallest=[user_key=key000001, seq=4, type=15],
|
||||
// largest= [user_key=key000002, seq=72057594037927935, type=15]
|
||||
// File[1]:
|
||||
// smallest=[user_key=key000002, seq=2, type=26],
|
||||
// largest= [user_key=key000003, seq=72057594037927935, type=15]
|
||||
// File[2]:
|
||||
// smallest=[user_key=key000003, seq=3, type=26],
|
||||
// largest= [user_key=key000004, seq=72057594037927935, type=15]
|
||||
//
|
||||
// After the fix:
|
||||
// File[0]:
|
||||
// smallest=[user_key=key000001, seq=4, type=15],
|
||||
// largest= [user_key=key000002, seq=72057594037927935, type=15]
|
||||
// File[1]:
|
||||
// smallest=[user_key=key000002, seq=2, type=1],
|
||||
// largest= [user_key=key000003, seq=72057594037927935, type=15]
|
||||
// File[2]:
|
||||
// smallest=[user_key=key000003, seq=3, type=1],
|
||||
// largest= [user_key=key000004, seq=72057594037927935, type=15]
|
||||
TEST_F(DBRangeDelTest, SeekForPrevTest) {
|
||||
// open db
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
// add SST partitioner, split sst file with prefix length 2
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(2);
|
||||
Reopen(options);
|
||||
|
||||
// Use partitioner that cuts before every new user key.
|
||||
// Key(x) generates keys of length 9.
|
||||
auto factory = std::shared_ptr<SstPartitionerFactory>(
|
||||
NewSstPartitionerFixedPrefixFactory(10));
|
||||
options.sst_partitioner_factory = factory;
|
||||
// File uses SST partitioner, so it will be split into 3 files
|
||||
// SST file 1: ka1, ka2
|
||||
// SST file 2: kb1
|
||||
// SST file 3: kc1, kc2
|
||||
// Delete range covers from ka2 to kc2, which means record ka2 and kb1, kc1
|
||||
// are covered by the delete range
|
||||
|
||||
DestroyAndReopen(options);
|
||||
std::vector<std::pair<std::string, std::string>> kv = {{"ka1", "value_1"},
|
||||
{"ka2", "value_2"},
|
||||
{"kb1", "value_3"},
|
||||
{"kc1", "value_4"},
|
||||
{"kc2", "value_5"}};
|
||||
for (auto& p : kv) {
|
||||
ASSERT_OK(Put(p.first, p.second));
|
||||
}
|
||||
|
||||
Random rnd(301);
|
||||
|
||||
// Create a file in a lower level so the compactions below are not
|
||||
// bottommost compactions. Range deletion start keys are not considered
|
||||
// in bottommost compaction.
|
||||
ASSERT_OK(Put(Key(3), rnd.RandomBinaryString(100)));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(6);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(6));
|
||||
// Compact to Lmax, it should have seq 0 now.
|
||||
ASSERT_OK(CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
ASSERT_OK(Put(Key(2), rnd.RandomString(100)));
|
||||
// Snapshots keep point keys alive.
|
||||
ManagedSnapshot snapshot1(db_);
|
||||
ASSERT_OK(Put(Key(3), rnd.RandomString(100)));
|
||||
ManagedSnapshot snapshot2(db_);
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(1),
|
||||
Key(4)));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
|
||||
ColumnFamilyMetaData cf_meta_l0;
|
||||
db_->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &cf_meta_l0);
|
||||
ASSERT_EQ(1, cf_meta_l0.levels[0].files.size());
|
||||
std::vector<std::string> l0_filenames;
|
||||
for (const auto& sst_file : cf_meta_l0.levels[0].files) {
|
||||
l0_filenames.push_back(sst_file.name);
|
||||
}
|
||||
|
||||
// Compact L0 files to L1
|
||||
CompactionOptions compact_options_l0;
|
||||
ASSERT_OK(db_->CompactFiles(compact_options_l0, l0_filenames, 1));
|
||||
ASSERT_EQ(3, NumTableFilesAtLevel(1));
|
||||
|
||||
// Check L1 file metadata
|
||||
std::vector<std::vector<FileMetaData>> files_l1;
|
||||
dbfull()->TEST_GetFilesMetaData(db_->DefaultColumnFamily(), &files_l1);
|
||||
|
||||
for (const auto& file : files_l1[1]) {
|
||||
ASSERT_LT(ExtractValueType(file.smallest.Encode()), kTypeMaxValid);
|
||||
ASSERT_LT(ExtractValueType(file.largest.Encode()), kTypeMaxValid);
|
||||
}
|
||||
|
||||
// Get file names from level 1
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
db_->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &cf_meta);
|
||||
std::vector<std::string> input_filenames;
|
||||
for (const auto& sst_file : cf_meta.levels[1].files) {
|
||||
input_filenames.push_back(sst_file.name);
|
||||
}
|
||||
|
||||
// Compact files from L1 to L2
|
||||
CompactionOptions compact_options;
|
||||
ASSERT_OK(db_->CompactFiles(compact_options, input_filenames, 2));
|
||||
|
||||
// Check L2 file metadata
|
||||
std::vector<std::vector<FileMetaData>> files;
|
||||
dbfull()->TEST_GetFilesMetaData(db_->DefaultColumnFamily(), &files);
|
||||
|
||||
for (const auto& file : files[2]) {
|
||||
ASSERT_LT(ExtractValueType(file.smallest.Encode()), kTypeMaxValid);
|
||||
ASSERT_LT(ExtractValueType(file.largest.Encode()), kTypeMaxValid);
|
||||
}
|
||||
|
||||
// // Verify iteration works correctly
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(ReadOptions())};
|
||||
// Open an iterator and create a snapshot, so that keys are not deleted
|
||||
// completely by delete range in SST
|
||||
ReadOptions read_opts;
|
||||
read_opts.snapshot = db_->GetSnapshot();
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_opts));
|
||||
iter->SeekToFirst();
|
||||
// iterate all the keys and validate the value
|
||||
for (int i = 0; iter->Valid(); iter->Next()) {
|
||||
ASSERT_EQ(kv[i].first, iter->key().ToString());
|
||||
ASSERT_EQ(kv[i].second, iter->value().ToString());
|
||||
i++;
|
||||
}
|
||||
|
||||
// use delete range to delete the record
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "ka2",
|
||||
"kc2"));
|
||||
// Flush
|
||||
ASSERT_OK(Flush());
|
||||
// Compact to Lmax
|
||||
ASSERT_OK(CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Close the iterator and release the snapshot.
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
iter.reset();
|
||||
db_->ReleaseSnapshot(read_opts.snapshot);
|
||||
|
||||
// create second iterator, seek each key and validate result
|
||||
std::unique_ptr<Iterator> iter2(db_->NewIterator(ReadOptions()));
|
||||
// Validate keys are deleted
|
||||
iter2->SeekToFirst();
|
||||
ASSERT_TRUE(iter2->Valid());
|
||||
ASSERT_EQ("ka1", iter2->key().ToString());
|
||||
iter2->Next();
|
||||
ASSERT_TRUE(iter2->Valid());
|
||||
ASSERT_EQ("kc2", iter2->key().ToString());
|
||||
iter2->Next();
|
||||
ASSERT_FALSE(iter2->Valid());
|
||||
|
||||
// Validate seek for prev result
|
||||
for (auto& p : kv) {
|
||||
iter2->SeekForPrev(p.first);
|
||||
ASSERT_TRUE(iter2->Valid());
|
||||
if (p.first == "kc2") {
|
||||
ASSERT_EQ("kc2", iter2->key().ToString());
|
||||
} else {
|
||||
ASSERT_EQ("ka1", iter2->key().ToString());
|
||||
}
|
||||
}
|
||||
ASSERT_OK(iter2->status());
|
||||
iter2.reset();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+5
-11
@@ -72,12 +72,6 @@ enum ValueType : unsigned char {
|
||||
kTypeColumnFamilyWideColumnEntity = 0x17, // WAL only
|
||||
kTypeValuePreferredSeqno = 0x18, // Value with a unix write time
|
||||
kTypeColumnFamilyValuePreferredSeqno = 0x19, // WAL only
|
||||
// Placeholder value type for legacy SST files with incorrectly persisted
|
||||
// file boundaries. Prior to the fix, TruncatedRangeDelIterator assigned
|
||||
// kTypeMaxValid to truncated range deletion keys, which was then
|
||||
// incorrectly persisted to SST file metadata. This placeholder value allows
|
||||
// reading such legacy files for without using kTypeMaxValid.
|
||||
kTypeTruncatedRangeDeletionSentinel = 0x1A,
|
||||
kTypeMaxValid, // Should be after the last valid type, only used for
|
||||
// validation
|
||||
kMaxValue = 0x7F // Not used for storing records.
|
||||
@@ -124,11 +118,10 @@ inline bool IsValueType(ValueType t) {
|
||||
|
||||
// Checks whether a type is from user operation
|
||||
// kTypeRangeDeletion is in meta block so this API is separated from above
|
||||
// kTypeTruncatedRangeDeletionSentinel is for legacy files with incorrectly
|
||||
// persisted file boundaries.
|
||||
// kTypeMaxValid can be from keys generated by
|
||||
// TruncatedRangeDelIterator::start_key()
|
||||
inline bool IsExtendedValueType(ValueType t) {
|
||||
return IsValueType(t) || t == kTypeRangeDeletion ||
|
||||
t == kTypeTruncatedRangeDeletionSentinel;
|
||||
return IsValueType(t) || t == kTypeRangeDeletion || t == kTypeMaxValid;
|
||||
}
|
||||
|
||||
// We leave eight bits empty at the bottom so a type and sequence#
|
||||
@@ -187,7 +180,8 @@ inline size_t InternalKeyEncodingLength(const ParsedInternalKey& key) {
|
||||
// Pack a sequence number and a ValueType into a uint64_t
|
||||
inline uint64_t PackSequenceAndType(uint64_t seq, ValueType t) {
|
||||
assert(seq <= kMaxSequenceNumber);
|
||||
assert(IsExtendedValueType(t));
|
||||
// kTypeMaxValid is used in TruncatedRangeDelIterator, see its constructor.
|
||||
assert(IsExtendedValueType(t) || t == kTypeMaxValid);
|
||||
return (seq << 8) | t;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ TruncatedRangeDelIterator::TruncatedRangeDelIterator(
|
||||
Status pik_status = ParseInternalKey(smallest->Encode(), &parsed_smallest,
|
||||
false /* log_err_key */); // TODO
|
||||
pik_status.PermitUncheckedError();
|
||||
parsed_smallest.type = kTypeMaxValid;
|
||||
assert(pik_status.ok());
|
||||
smallest_ = &parsed_smallest;
|
||||
}
|
||||
@@ -70,6 +71,9 @@ TruncatedRangeDelIterator::TruncatedRangeDelIterator(
|
||||
// the truncated end key can cover the largest key in this sstable, reduce
|
||||
// its sequence number by 1.
|
||||
parsed_largest.sequence -= 1;
|
||||
// This line is not needed for correctness, but it ensures that the
|
||||
// truncated end key is not covering keys from the next SST file.
|
||||
parsed_largest.type = kTypeMaxValid;
|
||||
}
|
||||
largest_ = &parsed_largest;
|
||||
}
|
||||
|
||||
@@ -89,9 +89,7 @@ void VerifyIterator(
|
||||
for (size_t i = 0; i < expected_range_dels.size(); i++, iter->Next()) {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
EXPECT_EQ(0, icmp.Compare(iter->start_key(), expected_range_dels[i].start));
|
||||
EXPECT_EQ(0, icmp.Compare(iter->end_key(), expected_range_dels[i].end))
|
||||
<< iter->end_key().DebugString(false, false) << " "
|
||||
<< expected_range_dels[i].end.DebugString(false, false);
|
||||
EXPECT_EQ(0, icmp.Compare(iter->end_key(), expected_range_dels[i].end));
|
||||
EXPECT_EQ(expected_range_dels[i].seq, iter->seq());
|
||||
}
|
||||
EXPECT_FALSE(iter->Valid());
|
||||
@@ -307,28 +305,28 @@ TEST_F(RangeDelAggregatorTest, TruncatedIterPartiallyCutTombstones) {
|
||||
|
||||
VerifyIterator(
|
||||
&iter, bytewise_icmp,
|
||||
{{InternalValue("d", 7, kTypeValue), UncutEndpoint("e"), 10},
|
||||
{{InternalValue("d", 7, kTypeMaxValid), UncutEndpoint("e"), 10},
|
||||
{InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{InternalValue("j", 4, kTypeRangeDeletion),
|
||||
InternalValue("m", 8, kTypeValue), 4}});
|
||||
InternalValue("m", 8, kTypeMaxValid), 4}});
|
||||
|
||||
VerifySeek(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", InternalValue("d", 7, kTypeValue), UncutEndpoint("e"), 10},
|
||||
{{"d", InternalValue("d", 7, kTypeMaxValid), UncutEndpoint("e"), 10},
|
||||
{"e", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"ia", InternalValue("j", 4, kTypeRangeDeletion),
|
||||
InternalValue("m", 8, kTypeValue), 4, false /* invalid */},
|
||||
InternalValue("m", 8, kTypeMaxValid), 4, false /* invalid */},
|
||||
{"n", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */},
|
||||
{"", InternalValue("d", 7, kTypeValue), UncutEndpoint("e"), 10}});
|
||||
{"", InternalValue("d", 7, kTypeMaxValid), UncutEndpoint("e"), 10}});
|
||||
|
||||
VerifySeekForPrev(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", InternalValue("d", 7, kTypeValue), UncutEndpoint("e"), 10},
|
||||
{{"d", InternalValue("d", 7, kTypeMaxValid), UncutEndpoint("e"), 10},
|
||||
{"e", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"ia", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"n", InternalValue("j", 4, kTypeRangeDeletion),
|
||||
InternalValue("m", 8, kTypeValue), 4, false /* invalid */},
|
||||
InternalValue("m", 8, kTypeMaxValid), 4, false /* invalid */},
|
||||
{"", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */}});
|
||||
}
|
||||
@@ -347,21 +345,23 @@ TEST_F(RangeDelAggregatorTest, TruncatedIterFullyCutTombstones) {
|
||||
TruncatedRangeDelIterator iter(std::move(input_iter), &bytewise_icmp,
|
||||
&smallest, &largest);
|
||||
|
||||
VerifyIterator(&iter, bytewise_icmp,
|
||||
{{InternalValue("f", 7, kTypeValue), UncutEndpoint("g"), 8}});
|
||||
VerifyIterator(
|
||||
&iter, bytewise_icmp,
|
||||
{{InternalValue("f", 7, kTypeMaxValid), UncutEndpoint("g"), 8}});
|
||||
|
||||
VerifySeek(&iter, bytewise_icmp,
|
||||
{{"d", InternalValue("f", 7, kTypeValue), UncutEndpoint("g"), 8},
|
||||
{"f", InternalValue("f", 7, kTypeValue), UncutEndpoint("g"), 8},
|
||||
{"j", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""),
|
||||
0, true /* invalid */}});
|
||||
VerifySeek(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", InternalValue("f", 7, kTypeMaxValid), UncutEndpoint("g"), 8},
|
||||
{"f", InternalValue("f", 7, kTypeMaxValid), UncutEndpoint("g"), 8},
|
||||
{"j", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */}});
|
||||
|
||||
VerifySeekForPrev(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */},
|
||||
{"f", InternalValue("f", 7, kTypeValue), UncutEndpoint("g"), 8},
|
||||
{"j", InternalValue("f", 7, kTypeValue), UncutEndpoint("g"), 8}});
|
||||
{"f", InternalValue("f", 7, kTypeMaxValid), UncutEndpoint("g"), 8},
|
||||
{"j", InternalValue("f", 7, kTypeMaxValid), UncutEndpoint("g"), 8}});
|
||||
}
|
||||
|
||||
TEST_F(RangeDelAggregatorTest, SingleIterInAggregator) {
|
||||
|
||||
@@ -30,7 +30,6 @@ uint64_t PackFileNumberAndPathId(uint64_t number, uint64_t path_id) {
|
||||
Status FileMetaData::UpdateBoundaries(const Slice& key, const Slice& value,
|
||||
SequenceNumber seqno,
|
||||
ValueType value_type) {
|
||||
assert(value_type < kTypeMaxValid);
|
||||
if (value_type == kTypeBlobIndex) {
|
||||
BlobIndex blob_index;
|
||||
const Status s = blob_index.DecodeFrom(value);
|
||||
|
||||
+7
-7
@@ -317,9 +317,6 @@ struct FileMetaData {
|
||||
void UpdateBoundariesForRange(const InternalKey& start,
|
||||
const InternalKey& end, SequenceNumber seqno,
|
||||
const InternalKeyComparator& icmp) {
|
||||
assert(ExtractValueType(start.Encode()) < kTypeMaxValid);
|
||||
assert(ExtractValueType(end.Encode()) < kTypeMaxValid);
|
||||
|
||||
if (smallest.size() == 0 || icmp.Compare(start, smallest) < 0) {
|
||||
smallest = start;
|
||||
}
|
||||
@@ -560,16 +557,19 @@ struct SubcompactionProgress {
|
||||
Slice key_slice(next_internal_key_to_compact);
|
||||
if (ParseInternalKey(key_slice, &parsed_key, false /* log_err_key */)
|
||||
.ok()) {
|
||||
oss << "user_key=\"" << parsed_key.user_key.ToString(false /* hex */)
|
||||
<< "\" (hex:" << parsed_key.user_key.ToString(true /* hex */)
|
||||
<< ")";
|
||||
oss << "user_key(hex)=" << parsed_key.user_key.ToString(true /* hex */);
|
||||
oss << ", seq=";
|
||||
if (parsed_key.sequence == kMaxSequenceNumber) {
|
||||
oss << "kMaxSequenceNumber";
|
||||
} else {
|
||||
oss << parsed_key.sequence;
|
||||
}
|
||||
oss << ", type=" << static_cast<int>(parsed_key.type);
|
||||
oss << ", type=";
|
||||
if (parsed_key.type == kValueTypeForSeek) {
|
||||
oss << "kValueTypeForSeek";
|
||||
} else {
|
||||
oss << static_cast<int>(parsed_key.type);
|
||||
}
|
||||
} else {
|
||||
oss << "raw=" << key_slice.ToString(true /* hex */);
|
||||
}
|
||||
|
||||
@@ -5571,6 +5571,15 @@ void VersionSet::Reset() {
|
||||
if (column_family_set_) {
|
||||
WriteBufferManager* wbm = column_family_set_->write_buffer_manager();
|
||||
WriteController* wc = column_family_set_->write_controller();
|
||||
|
||||
// Clear TableCache to prevent use-after-free: Reset() deletes old
|
||||
// ColumnFamilySet but reuses table_cache_, which may contain
|
||||
// BlockBasedTable entries with dangling references to deleted CFD's
|
||||
// ioptions.
|
||||
if (table_cache_) {
|
||||
table_cache_->EraseUnRefEntries();
|
||||
}
|
||||
|
||||
// db_id becomes the source of truth after DBImpl::Recover():
|
||||
// https://github.com/facebook/rocksdb/blob/v7.3.1/db/db_impl/db_impl_open.cc#L527
|
||||
// Note: we may not be able to recover db_id from MANIFEST if
|
||||
|
||||
Vendored
+126
-41
@@ -1655,42 +1655,6 @@ void GenerateFilesAndRequest(Env* env, const std::string& fname,
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(EnvPosixTest, MultiReadIOUringError) {
|
||||
// In this test we don't do aligned read, so we can't do direct I/O.
|
||||
EnvOptions soptions;
|
||||
soptions.use_direct_reads = soptions.use_direct_writes = false;
|
||||
std::string fname = test::PerThreadDBPath(env_, "testfile");
|
||||
|
||||
std::vector<std::string> scratches;
|
||||
std::vector<ReadRequest> reqs;
|
||||
GenerateFilesAndRequest(env_, fname, &reqs, &scratches);
|
||||
// Query the data
|
||||
std::unique_ptr<RandomAccessFile> file;
|
||||
ASSERT_OK(env_->NewRandomAccessFile(fname, &file, soptions));
|
||||
|
||||
bool io_uring_wait_cqe_called = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_wait_cqe:return",
|
||||
[&](void* arg) {
|
||||
if (!io_uring_wait_cqe_called) {
|
||||
io_uring_wait_cqe_called = true;
|
||||
ssize_t& ret = *(static_cast<ssize_t*>(arg));
|
||||
ret = 1;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s = file->MultiRead(reqs.data(), reqs.size());
|
||||
if (io_uring_wait_cqe_called) {
|
||||
ASSERT_NOK(s);
|
||||
} else {
|
||||
s.PermitUncheckedError();
|
||||
}
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(EnvPosixTest, MultiReadIOUringError2) {
|
||||
// In this test we don't do aligned read, so we can't do direct I/O.
|
||||
EnvOptions soptions;
|
||||
@@ -1706,19 +1670,20 @@ TEST_F(EnvPosixTest, MultiReadIOUringError2) {
|
||||
|
||||
bool io_uring_submit_and_wait_called = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return1",
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_sq_ready:return1",
|
||||
[&](void* arg) {
|
||||
io_uring_submit_and_wait_called = true;
|
||||
ssize_t* ret = static_cast<ssize_t*>(arg);
|
||||
(*ret)--;
|
||||
unsigned* ret = static_cast<unsigned*>(arg);
|
||||
*ret = 1;
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return2",
|
||||
[&](void* arg) {
|
||||
struct io_uring* iu = static_cast<struct io_uring*>(arg);
|
||||
struct io_uring_cqe* cqe;
|
||||
assert(io_uring_wait_cqe(iu, &cqe) == 0);
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
// CQ should be empty after drain - peek should fail
|
||||
int ret = io_uring_peek_cqe(iu, &cqe);
|
||||
assert(-EAGAIN == ret); // No CQEs available
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
@@ -3640,6 +3605,126 @@ TEST_F(TestAsyncRead, ReadAsync) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test ReadAsync -> MultiRead -> Poll with real io_uring (not mock).
|
||||
// This verifies that MultiRead doesn't interfere with async read buffers.
|
||||
TEST_F(TestAsyncRead, InterleavingIOUringOperations) {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// Use the real filesystem directly (not the mock ReadAsyncFS).
|
||||
std::shared_ptr<FileSystem> fs = env_->GetFileSystem();
|
||||
std::string fname = test::PerThreadDBPath(env_, "testfile_iouring");
|
||||
|
||||
constexpr size_t kSectorSize = 4096;
|
||||
constexpr size_t kNumSectors = 8;
|
||||
|
||||
// 1. Create & write to a file.
|
||||
{
|
||||
std::unique_ptr<FSWritableFile> wfile;
|
||||
ASSERT_OK(
|
||||
fs->NewWritableFile(fname, FileOptions(), &wfile, nullptr /*dbg*/));
|
||||
|
||||
for (size_t i = 0; i < kNumSectors; ++i) {
|
||||
auto data = NewAligned(kSectorSize * 8, static_cast<char>(i + 1));
|
||||
Slice slice(data.get(), kSectorSize);
|
||||
ASSERT_OK(wfile->Append(slice, IOOptions(), nullptr));
|
||||
}
|
||||
ASSERT_OK(wfile->Close(IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
// 2. Test interleaved ReadAsync and MultiRead operations.
|
||||
{
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
ASSERT_OK(fs->NewRandomAccessFile(fname, FileOptions(), &file, nullptr));
|
||||
|
||||
IOOptions opts;
|
||||
std::vector<void*> io_handles(kNumSectors);
|
||||
std::vector<FSReadRequest> async_reqs(kNumSectors);
|
||||
std::vector<std::unique_ptr<char, Deleter>> async_data;
|
||||
std::vector<size_t> vals;
|
||||
IOHandleDeleter del_fn;
|
||||
|
||||
// Initialize async read requests.
|
||||
for (size_t i = 0; i < kNumSectors; i++) {
|
||||
async_reqs[i].offset = i * kSectorSize;
|
||||
async_reqs[i].len = kSectorSize;
|
||||
async_data.emplace_back(NewAligned(kSectorSize, 0));
|
||||
async_reqs[i].scratch = async_data.back().get();
|
||||
vals.push_back(i);
|
||||
}
|
||||
|
||||
// Callback function for async reads.
|
||||
std::function<void(FSReadRequest&, void*)> callback =
|
||||
[&](FSReadRequest& req, void* cb_arg) {
|
||||
assert(cb_arg != nullptr);
|
||||
size_t i = *(reinterpret_cast<size_t*>(cb_arg));
|
||||
async_reqs[i].offset = req.offset;
|
||||
async_reqs[i].result = req.result;
|
||||
async_reqs[i].status = req.status;
|
||||
};
|
||||
|
||||
// Submit asynchronous read requests.
|
||||
for (size_t i = 0; i < kNumSectors; i++) {
|
||||
void* cb_arg = static_cast<void*>(&(vals[i]));
|
||||
IOStatus s = file->ReadAsync(async_reqs[i], opts, callback, cb_arg,
|
||||
&(io_handles[i]), &del_fn, nullptr);
|
||||
if (s.IsNotSupported()) {
|
||||
// io_uring not supported on this system, skip the test.
|
||||
fprintf(stderr, "Skipping test - io_uring not supported: %s\n",
|
||||
s.ToString().c_str());
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
if (io_handles[j] != nullptr) {
|
||||
del_fn(io_handles[j]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// For any other error, fail the test.
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Do a MultiRead on same sectors while async reads are submitted.
|
||||
std::vector<FSReadRequest> multi_reqs(kNumSectors);
|
||||
std::vector<std::unique_ptr<char, Deleter>> multi_data;
|
||||
for (size_t i = 0; i < kNumSectors; i++) {
|
||||
multi_reqs[i].offset = i * kSectorSize;
|
||||
multi_reqs[i].len = kSectorSize;
|
||||
multi_data.emplace_back(NewAligned(kSectorSize, 0));
|
||||
multi_reqs[i].scratch = multi_data.back().get();
|
||||
}
|
||||
ASSERT_OK(file->MultiRead(multi_reqs.data(), kNumSectors, opts, nullptr));
|
||||
|
||||
// Check the status of MultiRead requests (should all succeed).
|
||||
for (size_t i = 0; i < kNumSectors; i++) {
|
||||
auto buf = NewAligned(kSectorSize * 8, static_cast<char>(i + 1));
|
||||
Slice expected_data(buf.get(), kSectorSize);
|
||||
|
||||
ASSERT_EQ(multi_reqs[i].offset, i * kSectorSize);
|
||||
ASSERT_OK(multi_reqs[i].status);
|
||||
ASSERT_EQ(expected_data.ToString(), multi_reqs[i].result.ToString());
|
||||
}
|
||||
|
||||
// Poll for the submitted async requests.
|
||||
ASSERT_OK(fs->Poll(io_handles, kNumSectors));
|
||||
|
||||
// Check the status of async read requests (should all succeed).
|
||||
for (size_t i = 0; i < kNumSectors; i++) {
|
||||
auto buf = NewAligned(kSectorSize * 8, static_cast<char>(i + 1));
|
||||
Slice expected_data(buf.get(), kSectorSize);
|
||||
|
||||
ASSERT_EQ(async_reqs[i].offset, i * kSectorSize);
|
||||
ASSERT_OK(async_reqs[i].status);
|
||||
ASSERT_EQ(expected_data.ToString(), async_reqs[i].result.ToString());
|
||||
}
|
||||
|
||||
// Delete io_handles.
|
||||
for (size_t i = 0; i < io_handles.size(); i++) {
|
||||
del_fn(io_handles[i]);
|
||||
}
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "Skipping test - ROCKSDB_IOURING_PRESENT not defined\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
struct StaticDestructionTester {
|
||||
bool activated = false;
|
||||
~StaticDestructionTester() {
|
||||
|
||||
Vendored
+14
-7
@@ -270,7 +270,10 @@ class PosixFileSystem : public FileSystem {
|
||||
options
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
!IsIOUringEnabled() ? nullptr : thread_local_io_urings_.get()
|
||||
!IsIOUringEnabled() ? nullptr
|
||||
: thread_local_async_read_io_urings_.get(),
|
||||
!IsIOUringEnabled() ? nullptr
|
||||
: thread_local_multi_read_io_urings_.get()
|
||||
#endif
|
||||
));
|
||||
}
|
||||
@@ -1087,8 +1090,9 @@ class PosixFileSystem : public FileSystem {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring_queue_init.
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
if (thread_local_async_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_async_read_io_urings_->Get());
|
||||
}
|
||||
|
||||
// Init failed, platform doesn't support io_uring.
|
||||
@@ -1161,8 +1165,9 @@ class PosixFileSystem : public FileSystem {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring_queue_init.
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
if (thread_local_async_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_async_read_io_urings_->Get());
|
||||
}
|
||||
|
||||
// Init failed, platform doesn't support io_uring.
|
||||
@@ -1277,7 +1282,8 @@ class PosixFileSystem : public FileSystem {
|
||||
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring instance
|
||||
std::unique_ptr<ThreadLocalPtr> thread_local_io_urings_;
|
||||
std::unique_ptr<ThreadLocalPtr> thread_local_async_read_io_urings_;
|
||||
std::unique_ptr<ThreadLocalPtr> thread_local_multi_read_io_urings_;
|
||||
#endif
|
||||
|
||||
size_t page_size_;
|
||||
@@ -1337,7 +1343,8 @@ PosixFileSystem::PosixFileSystem()
|
||||
// io_uring can be created.
|
||||
struct io_uring* new_io_uring = CreateIOUring();
|
||||
if (new_io_uring != nullptr) {
|
||||
thread_local_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
|
||||
thread_local_async_read_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
|
||||
thread_local_multi_read_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
|
||||
delete new_io_uring;
|
||||
}
|
||||
#endif
|
||||
|
||||
Vendored
+303
-112
@@ -589,7 +589,8 @@ PosixRandomAccessFile::PosixRandomAccessFile(
|
||||
const EnvOptions& options
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
ThreadLocalPtr* thread_local_io_urings
|
||||
ThreadLocalPtr* thread_local_async_read_io_urings,
|
||||
ThreadLocalPtr* thread_local_multi_read_io_urings
|
||||
#endif
|
||||
)
|
||||
: filename_(fname),
|
||||
@@ -598,7 +599,8 @@ PosixRandomAccessFile::PosixRandomAccessFile(
|
||||
logical_sector_size_(logical_block_size)
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
thread_local_io_urings_(thread_local_io_urings)
|
||||
thread_local_async_read_io_urings_(thread_local_async_read_io_urings),
|
||||
thread_local_multi_read_io_urings_(thread_local_multi_read_io_urings)
|
||||
#endif
|
||||
{
|
||||
assert(!options.use_direct_reads || !options.use_mmap_reads);
|
||||
@@ -659,6 +661,83 @@ IOStatus PosixRandomAccessFile::Read(uint64_t offset, size_t n,
|
||||
return s;
|
||||
}
|
||||
|
||||
// MultiRead: Perform multiple concurrent read requests using io_uring.
|
||||
//
|
||||
// OVERVIEW:
|
||||
// This function batches multiple read requests and submits them concurrently
|
||||
// to io_uring for improved I/O performance. It operates synchronously from the
|
||||
// caller's perspective (blocks until all reads complete) but uses io_uring's
|
||||
// async capabilities internally for parallel I/O execution.
|
||||
//
|
||||
// IO_URING LIFECYCLE:
|
||||
// 1. Preparation Phase:
|
||||
// - Allocate SQEs (Submission Queue Entries) for read requests
|
||||
// - Limited by: min(pending_work, io_uring_sq_space_left(), kIoUringDepth -
|
||||
// inflight)
|
||||
// - Uses io_uring_sq_space_left() to query available SQ slots
|
||||
// - Each SQE is tracked in wrap_cache for completion matching
|
||||
//
|
||||
// 2. Submission Phase:
|
||||
// - Loop: while io_uring_sq_ready() > 0 (SQEs pending submission)
|
||||
// - Call io_uring_submit_and_wait() to submit SQEs and wait for CQEs
|
||||
// - Handles retryable errors (EINTR, EAGAIN) by continuing
|
||||
// - Breaks on terminal errors (logs error, sets err variable)
|
||||
//
|
||||
// 3. Completion Phase:
|
||||
// - Non-blocking CQE reaping via io_uring_for_each_cqe()
|
||||
// - Matches CQEs to requests using user_data pointer
|
||||
// - Processes results: updates bytes read, handles partial reads
|
||||
// - Removes completed requests from wrap_cache
|
||||
//
|
||||
// 4. Loop Iteration:
|
||||
// - Repeats until: all requests submitted AND all completions reaped
|
||||
// - Termination condition: (num_reqs == reqs_off) &&
|
||||
// resubmit_rq_list.empty() && wrap_cache.empty()
|
||||
//
|
||||
// ERROR HANDLING STRATEGY:
|
||||
// - Retryable submission errors (-EINTR, -EAGAIN): Retry submission
|
||||
// - Memory pressure (-ENOMEM): Mark memory_pressure_on_submission, attempt
|
||||
// recovery
|
||||
// - Terminal submission errors: Break, enter teardown path
|
||||
// - Retryable CQE errors (-EINTR, -EAGAIN): Add to resubmit_rq_list for retry
|
||||
// - Terminal CQE errors: Set ios to IOError, continue processing other CQEs
|
||||
// - Teardown path: If SQEs remain unsubmitted after error, reap submitted CQEs,
|
||||
// destroy io_uring instance, return error
|
||||
//
|
||||
// PARTIAL READ HANDLING:
|
||||
// - Short reads (bytes_read < requested): Request added to resubmit_rq_list
|
||||
// - finished_len tracks cumulative bytes read across resubmissions
|
||||
// - iov.iov_base/iov_len adjusted on each resubmission attempt
|
||||
// - UpdateResult() determines if read should be retried based on:
|
||||
// * Direct I/O alignment requirements
|
||||
// * EOF detection
|
||||
// * Error conditions
|
||||
//
|
||||
// RESUBMISSION LOGIC:
|
||||
// - resubmit_rq_list: Requests needing retry (short reads, EINTR/EAGAIN errors)
|
||||
// - Prioritized in SQE allocation loop: resubmits before new requests
|
||||
// - List cleared after SQE preparation
|
||||
// - Requests remain in wrap_cache across resubmissions until fully complete
|
||||
//
|
||||
// CONCURRENCY CONTROL:
|
||||
// - wrap_cache.size(): Tracks total inflight requests (SQ + CQ)
|
||||
// - io_uring_sq_ready(): Queries SQEs prepared but not yet submitted
|
||||
// - io_uring_sq_space_left(): Queries available SQ slots
|
||||
// - Max concurrency: kIoUringDepth (256)
|
||||
//
|
||||
// ACCOUNTING CORRECTNESS:
|
||||
// - Uses io_uring native APIs (io_uring_sq_ready, io_uring_sq_space_left)
|
||||
// instead of manual counters for robustness
|
||||
// - wrap_cache is the authoritative source for inflight request tracking
|
||||
// - Re-query io_uring_sq_ready() after submission loop to detect
|
||||
// unsubmitted SQEs (indicates submission errors)
|
||||
//
|
||||
// THREAD SAFETY:
|
||||
// - Uses thread-local io_uring instance (thread_local_multi_read_io_urings_)
|
||||
// - IORING_SETUP_SINGLE_ISSUER: Only one thread submits to this ring
|
||||
// - IORING_SETUP_DEFER_TASKRUN: Task work runs in submitting thread
|
||||
// - No cross-thread coordination required
|
||||
//
|
||||
IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
const IOOptions& options,
|
||||
IODebugContext* dbg) {
|
||||
@@ -672,12 +751,16 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
if (thread_local_multi_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_multi_read_io_urings_->Get());
|
||||
if (iu == nullptr) {
|
||||
iu = CreateIOUring();
|
||||
unsigned int flags = 0;
|
||||
flags |= IORING_SETUP_SINGLE_ISSUER;
|
||||
flags |= IORING_SETUP_DEFER_TASKRUN;
|
||||
iu = CreateIOUring(flags);
|
||||
if (iu != nullptr) {
|
||||
thread_local_io_urings_->Reset(iu);
|
||||
thread_local_multi_read_io_urings_->Reset(iu);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -688,8 +771,6 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
return FSRandomAccessFile::MultiRead(reqs, num_reqs, options, dbg);
|
||||
}
|
||||
|
||||
IOStatus ios = IOStatus::OK();
|
||||
|
||||
struct WrappedReadRequest {
|
||||
FSReadRequest* req;
|
||||
struct iovec iov;
|
||||
@@ -698,118 +779,199 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
};
|
||||
|
||||
autovector<WrappedReadRequest, 32> req_wraps;
|
||||
autovector<WrappedReadRequest*, 4> incomplete_rq_list;
|
||||
autovector<WrappedReadRequest*, 4> resubmit_rq_list;
|
||||
std::unordered_set<WrappedReadRequest*> wrap_cache;
|
||||
|
||||
for (size_t i = 0; i < num_reqs; i++) {
|
||||
req_wraps.emplace_back(&reqs[i]);
|
||||
}
|
||||
|
||||
IOStatus ios = IOStatus::OK();
|
||||
size_t reqs_off = 0;
|
||||
while (num_reqs > reqs_off || !incomplete_rq_list.empty()) {
|
||||
size_t this_reqs = (num_reqs - reqs_off) + incomplete_rq_list.size();
|
||||
|
||||
// If requests exceed depth, split it into batches
|
||||
if (this_reqs > kIoUringDepth) {
|
||||
this_reqs = kIoUringDepth;
|
||||
}
|
||||
|
||||
assert(incomplete_rq_list.size() <= this_reqs);
|
||||
for (size_t i = 0; i < this_reqs; i++) {
|
||||
WrappedReadRequest* rep_to_submit;
|
||||
if (i < incomplete_rq_list.size()) {
|
||||
rep_to_submit = incomplete_rq_list[i];
|
||||
while ((num_reqs > reqs_off) || !resubmit_rq_list.empty() ||
|
||||
!wrap_cache.empty()) {
|
||||
assert(resubmit_rq_list.size() + wrap_cache.size() <= kIoUringDepth);
|
||||
// Total number of requests that still need to be submitted, includes:
|
||||
//
|
||||
// 1) requests NOT yet submitted (num_reqs - reqs_off)
|
||||
// 2) requests on resubmission list (resubmit_rq_list)
|
||||
//
|
||||
// capped by min of the # of remaining entries in IO ring submission queue
|
||||
// and the max IO ring depth less the inflight requests.
|
||||
size_t new_sqe_reqs_count = std::min({
|
||||
num_reqs - reqs_off + resubmit_rq_list.size(),
|
||||
static_cast<size_t>(io_uring_sq_space_left(iu)),
|
||||
kIoUringDepth - wrap_cache.size() // queue depth less inflight requests
|
||||
});
|
||||
for (size_t i = 0; i < new_sqe_reqs_count; i++) {
|
||||
WrappedReadRequest* req;
|
||||
if (i < resubmit_rq_list.size()) {
|
||||
req = resubmit_rq_list[i];
|
||||
} else {
|
||||
rep_to_submit = &req_wraps[reqs_off++];
|
||||
req = &req_wraps[reqs_off++];
|
||||
}
|
||||
assert(rep_to_submit->req->len > rep_to_submit->finished_len);
|
||||
rep_to_submit->iov.iov_base =
|
||||
rep_to_submit->req->scratch + rep_to_submit->finished_len;
|
||||
rep_to_submit->iov.iov_len =
|
||||
rep_to_submit->req->len - rep_to_submit->finished_len;
|
||||
assert(req->req->len > req->finished_len);
|
||||
req->iov.iov_base = req->req->scratch + req->finished_len;
|
||||
req->iov.iov_len = req->req->len - req->finished_len;
|
||||
|
||||
struct io_uring_sqe* sqe;
|
||||
sqe = io_uring_get_sqe(iu);
|
||||
io_uring_prep_readv(
|
||||
sqe, fd_, &rep_to_submit->iov, 1,
|
||||
rep_to_submit->req->offset + rep_to_submit->finished_len);
|
||||
io_uring_sqe_set_data(sqe, rep_to_submit);
|
||||
wrap_cache.emplace(rep_to_submit);
|
||||
// NULL is unexpected as we do maintain proper ring accounting.
|
||||
assert(sqe);
|
||||
io_uring_prep_readv(sqe, fd_, &req->iov, 1,
|
||||
req->req->offset + req->finished_len);
|
||||
io_uring_sqe_set_data(sqe, req);
|
||||
wrap_cache.emplace(req);
|
||||
}
|
||||
incomplete_rq_list.clear();
|
||||
resubmit_rq_list.clear();
|
||||
|
||||
ssize_t ret =
|
||||
io_uring_submit_and_wait(iu, static_cast<unsigned int>(this_reqs));
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return1",
|
||||
&ret);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return2",
|
||||
iu);
|
||||
|
||||
if (static_cast<size_t>(ret) != this_reqs) {
|
||||
fprintf(stderr, "ret = %ld this_reqs: %ld\n", (long)ret, (long)this_reqs);
|
||||
// If error happens and we submitted fewer than expected, it is an
|
||||
// exception case and we don't retry here. We should still consume
|
||||
// what is is submitted in the ring.
|
||||
for (ssize_t i = 0; i < ret; i++) {
|
||||
struct io_uring_cqe* cqe = nullptr;
|
||||
io_uring_wait_cqe(iu, &cqe);
|
||||
if (cqe != nullptr) {
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
struct io_uring_cqe* cqe = nullptr;
|
||||
unsigned head;
|
||||
ssize_t err = 0;
|
||||
bool memory_pressure_on_submission = false;
|
||||
unsigned reqs_pending_submission;
|
||||
unsigned reqs_submitted = 0;
|
||||
while ((reqs_pending_submission = io_uring_sq_ready(iu))) {
|
||||
// MultiRead is synchronous in nature. io_uring_submit_and_wait provides
|
||||
// batching semantics (submit + best effort wait in one syscall), while
|
||||
// io_uring_submit enables async producer/consumer semantics (submit
|
||||
// only, requires separate reaping). We chose batching approach to
|
||||
// reduce the volume of syscalls and context switches.
|
||||
ssize_t ret = io_uring_submit_and_wait(iu, reqs_pending_submission);
|
||||
if (ret < 0) {
|
||||
if (-EINTR == ret || -EAGAIN == ret) {
|
||||
// Submission failed due to rare, retryable syscall error. Try again.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return IOStatus::IOError("io_uring_submit_and_wait() requested " +
|
||||
std::to_string(this_reqs) + " but returned " +
|
||||
std::to_string(ret));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < this_reqs; i++) {
|
||||
struct io_uring_cqe* cqe = nullptr;
|
||||
WrappedReadRequest* req_wrap;
|
||||
|
||||
// We could use the peek variant here, but this seems safer in terms
|
||||
// of our initial wait not reaping all completions
|
||||
ret = io_uring_wait_cqe(iu, &cqe);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_wait_cqe:return", &ret);
|
||||
if (ret) {
|
||||
ios = IOStatus::IOError("io_uring_wait_cqe() returns " +
|
||||
std::to_string(ret));
|
||||
|
||||
if (cqe != nullptr) {
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
if (-ENOMEM == ret) {
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::MultiRead: io_uring_submit_and_wait "
|
||||
"experienced terse memory condition.\n");
|
||||
// Best effort to reclaim resources in terse condition.
|
||||
memory_pressure_on_submission = true;
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::MultiRead: "
|
||||
"io_uring_submit_and_wait returned terminal error: %zd.\n",
|
||||
ret);
|
||||
err = ret;
|
||||
}
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
req_wrap = static_cast<WrappedReadRequest*>(io_uring_cqe_get_data(cqe));
|
||||
// Reset cqe data to catch any stray reuse of it
|
||||
static_cast<struct io_uring_cqe*>(cqe)->user_data = 0xd5d5d5d5d5d5d5d5;
|
||||
// Check that we got a valid unique cqe data
|
||||
auto wrap_check = wrap_cache.find(req_wrap);
|
||||
if (wrap_check == wrap_cache.end()) {
|
||||
if (0 == ret) {
|
||||
// This scenario is unexpected for any modern kernel!
|
||||
// We deliberately error out to avoid bugs around infinite loops.
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::MultiRead: "
|
||||
"Bad cqe data from IO uring - %p\n",
|
||||
req_wrap);
|
||||
port::PrintStack();
|
||||
ios = IOStatus::IOError("io_uring_cqe_get_data() returned " +
|
||||
std::to_string((uint64_t)req_wrap));
|
||||
continue;
|
||||
"io_uring_submit_and_wait returned 0 submissions!\n");
|
||||
break;
|
||||
}
|
||||
wrap_cache.erase(wrap_check);
|
||||
reqs_submitted += static_cast<unsigned int>(ret);
|
||||
};
|
||||
reqs_pending_submission = io_uring_sq_ready(iu);
|
||||
|
||||
FSReadRequest* req = req_wrap->req;
|
||||
size_t bytes_read = 0;
|
||||
bool read_again = false;
|
||||
UpdateResult(cqe, filename_, req->len, req_wrap->iov.iov_len,
|
||||
false /*async_read*/, use_direct_io(),
|
||||
GetRequiredBufferAlignment(), req_wrap->finished_len, req,
|
||||
bytes_read, read_again);
|
||||
int32_t res = cqe->res;
|
||||
if (res >= 0) {
|
||||
if (bytes_read == 0) {
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_sq_ready:return1",
|
||||
&reqs_pending_submission);
|
||||
|
||||
// Error occurred or IO uring stopped submitting outstanding requests.
|
||||
if (reqs_pending_submission && !memory_pressure_on_submission) {
|
||||
// IO ring is initialized once in thread-local variable and then reused
|
||||
// to handle the consecutive MultiRead API calls. Therefore, it's crucial
|
||||
// to reap all the submitted requests.
|
||||
//
|
||||
// NOTE: Loop will run indefinitely until we reap all the completions!!!
|
||||
size_t nr = 0;
|
||||
assert(reqs_pending_submission <= wrap_cache.size());
|
||||
size_t nr_await_cqe = wrap_cache.size() - reqs_pending_submission;
|
||||
while (nr < nr_await_cqe) {
|
||||
// blocking
|
||||
io_uring_wait_cqes(iu, &cqe,
|
||||
static_cast<unsigned int>(nr_await_cqe - nr),
|
||||
nullptr, nullptr);
|
||||
size_t reaped_cqe_count = 0;
|
||||
io_uring_for_each_cqe(iu, head, cqe) { reaped_cqe_count++; }
|
||||
if (reaped_cqe_count > 0) {
|
||||
io_uring_cq_advance(iu, static_cast<unsigned int>(reaped_cqe_count));
|
||||
nr += reaped_cqe_count;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return2",
|
||||
iu);
|
||||
|
||||
// While all the submitted completions have been reaped successfully,
|
||||
// IO ring submission queue still contains at least one non-submitted
|
||||
// request. Destroy io_uring (discards unsubmitted SQEs).
|
||||
//
|
||||
// NOTE: This is a rare scenario and should not happen in normal cases.
|
||||
// Hence, this should NOT materially impact the performance metrics.
|
||||
io_uring_queue_exit(iu);
|
||||
delete iu;
|
||||
thread_local_multi_read_io_urings_->Reset(nullptr);
|
||||
|
||||
if (err < 0) {
|
||||
return IOStatus::IOError(
|
||||
"io_uring_submit_and_wait() failed with an error " +
|
||||
std::to_string(err));
|
||||
}
|
||||
return IOStatus::IOError(
|
||||
"io_uring_submit_and_wait() requested " +
|
||||
std::to_string(reqs_submitted + reqs_pending_submission) +
|
||||
" but returned " + std::to_string(reqs_submitted));
|
||||
}
|
||||
|
||||
if ((0 == reqs_submitted) && wrap_cache.size() > reqs_pending_submission) {
|
||||
// If no requests have been submitted and there is at least one request
|
||||
// pending completion, wait for at least one completion to arrive.
|
||||
// This is a guardrail to prevent the busy CPU loops.
|
||||
//
|
||||
// NOTE: it's not really a tight CPU-burning loop in the traditional sense
|
||||
// as it's naturally throttled by the io_uring_submit_and_wait() syscall.
|
||||
io_uring_wait_cqe(iu, &cqe);
|
||||
}
|
||||
|
||||
unsigned int nr = 0;
|
||||
io_uring_for_each_cqe(iu, head, cqe) { // non-blocking
|
||||
if (cqe->user_data) { // non-discarded, valid user data only!
|
||||
nr++;
|
||||
WrappedReadRequest* req_wrap =
|
||||
static_cast<WrappedReadRequest*>(io_uring_cqe_get_data(cqe));
|
||||
// Reset cqe data to catch any stray reuse of it
|
||||
static_cast<struct io_uring_cqe*>(cqe)->user_data = 0xd5d5d5d5d5d5d5d5;
|
||||
// Check that we got a valid unique cqe data
|
||||
auto wrap_check = wrap_cache.find(req_wrap);
|
||||
if (wrap_check == wrap_cache.end()) {
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::MultiRead: "
|
||||
"Bad cqe data from IO uring - %p\n",
|
||||
req_wrap);
|
||||
port::PrintStack();
|
||||
ios = IOStatus::IOError("io_uring_cqe_get_data() returned " +
|
||||
std::to_string((uint64_t)req_wrap));
|
||||
continue;
|
||||
}
|
||||
wrap_cache.erase(wrap_check);
|
||||
if (cqe->res < 0) {
|
||||
if (-EINTR == cqe->res || -EAGAIN == cqe->res) {
|
||||
resubmit_rq_list.push_back(req_wrap);
|
||||
} else {
|
||||
ios = IOStatus::IOError("io_uring_for_each_cqe() returns " +
|
||||
std::to_string(cqe->res));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// cqe->res >= 0
|
||||
FSReadRequest* req = req_wrap->req;
|
||||
size_t bytes_read = 0;
|
||||
bool read_again = false;
|
||||
UpdateResult(cqe, filename_, req->len, req_wrap->iov.iov_len,
|
||||
false /*async_read*/, use_direct_io(),
|
||||
GetRequiredBufferAlignment(), req_wrap->finished_len, req,
|
||||
bytes_read, read_again);
|
||||
|
||||
if (0 == bytes_read) {
|
||||
if (read_again) {
|
||||
Slice tmp_slice;
|
||||
req->status =
|
||||
@@ -819,14 +981,15 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
req->result =
|
||||
Slice(req->scratch, req_wrap->finished_len + tmp_slice.size());
|
||||
}
|
||||
// else It means EOF so no need to do anything.
|
||||
// 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);
|
||||
resubmit_rq_list.push_back(req_wrap);
|
||||
}
|
||||
}
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
}
|
||||
wrap_cache.clear();
|
||||
if (nr > 0) {
|
||||
io_uring_cq_advance(iu, nr);
|
||||
}
|
||||
}
|
||||
return ios;
|
||||
#else
|
||||
@@ -923,12 +1086,16 @@ IOStatus PosixRandomAccessFile::ReadAsync(
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring_queue_init.
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
if (thread_local_async_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_async_read_io_urings_->Get());
|
||||
if (iu == nullptr) {
|
||||
iu = CreateIOUring();
|
||||
unsigned int flags = 0;
|
||||
flags |= IORING_SETUP_SINGLE_ISSUER;
|
||||
flags |= IORING_SETUP_DEFER_TASKRUN;
|
||||
iu = CreateIOUring(flags);
|
||||
if (iu != nullptr) {
|
||||
thread_local_io_urings_->Reset(iu);
|
||||
thread_local_async_read_io_urings_->Reset(iu);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -966,11 +1133,35 @@ IOStatus PosixRandomAccessFile::ReadAsync(
|
||||
io_uring_sqe_set_data(sqe, posix_handle);
|
||||
|
||||
// Step 4: io_uring_submit
|
||||
ssize_t ret = io_uring_submit(iu);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "io_uring_submit error: %ld\n", long(ret));
|
||||
return IOStatus::IOError("io_uring_submit() requested but returned " +
|
||||
std::to_string(ret));
|
||||
ssize_t ret;
|
||||
do {
|
||||
ret = io_uring_submit(iu);
|
||||
if (ret < 0) {
|
||||
if (-EINTR == ret || -EAGAIN == ret) {
|
||||
// Submission failed due to transient error. Try again.
|
||||
continue;
|
||||
}
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::ReadAsync: "
|
||||
"io_uring_submit returned terminal error = %zd\n",
|
||||
ret);
|
||||
break;
|
||||
}
|
||||
if (0 == ret) {
|
||||
// Unexpected. Will be reported as error.
|
||||
break;
|
||||
}
|
||||
} while (ret < 1);
|
||||
if (ret <= 0) {
|
||||
return IOStatus::IOError(
|
||||
"PosixRandomAccessFile::ReadAsync: io_uring_submit() returned " +
|
||||
std::to_string(ret));
|
||||
}
|
||||
if (ret > 1) {
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::ReadAsync: "
|
||||
"io_uring_submit() returned = %zd\n",
|
||||
ret);
|
||||
}
|
||||
return IOStatus::OK();
|
||||
#else
|
||||
|
||||
Vendored
+16
-4
@@ -11,6 +11,16 @@
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
#include <liburing.h>
|
||||
#include <sys/uio.h>
|
||||
|
||||
// Compatibility defines for io_uring flags that may not be present in older
|
||||
// kernel headers. These values are fixed and won't change, so it's safe to
|
||||
// define them even if the running kernel doesn't support them.
|
||||
#ifndef IORING_SETUP_SINGLE_ISSUER
|
||||
#define IORING_SETUP_SINGLE_ISSUER (1U << 12)
|
||||
#endif
|
||||
#ifndef IORING_SETUP_DEFER_TASKRUN
|
||||
#define IORING_SETUP_DEFER_TASKRUN (1U << 13)
|
||||
#endif
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -297,9 +307,9 @@ inline void DeleteIOUring(void* p) {
|
||||
delete iu;
|
||||
}
|
||||
|
||||
inline struct io_uring* CreateIOUring() {
|
||||
inline struct io_uring* CreateIOUring(unsigned int flags = 0) {
|
||||
struct io_uring* new_io_uring = new struct io_uring;
|
||||
int ret = io_uring_queue_init(kIoUringDepth, new_io_uring, 0);
|
||||
int ret = io_uring_queue_init(kIoUringDepth, new_io_uring, flags);
|
||||
if (ret) {
|
||||
delete new_io_uring;
|
||||
new_io_uring = nullptr;
|
||||
@@ -315,7 +325,8 @@ class PosixRandomAccessFile : public FSRandomAccessFile {
|
||||
bool use_direct_io_;
|
||||
size_t logical_sector_size_;
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
ThreadLocalPtr* thread_local_io_urings_;
|
||||
ThreadLocalPtr* thread_local_async_read_io_urings_;
|
||||
ThreadLocalPtr* thread_local_multi_read_io_urings_;
|
||||
#endif
|
||||
|
||||
public:
|
||||
@@ -323,7 +334,8 @@ class PosixRandomAccessFile : public FSRandomAccessFile {
|
||||
size_t logical_block_size, const EnvOptions& options
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
ThreadLocalPtr* thread_local_io_urings
|
||||
ThreadLocalPtr* thread_local_async_read_io_urings,
|
||||
ThreadLocalPtr* thread_local_multi_read_io_urings
|
||||
#endif
|
||||
);
|
||||
virtual ~PosixRandomAccessFile();
|
||||
|
||||
@@ -125,7 +125,7 @@ class Compressor {
|
||||
// dictionary associated with a returned compressor must be read from
|
||||
// GetSerializedDict().
|
||||
virtual std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) {
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
|
||||
// Default implementation: no specialization
|
||||
(void)block_type;
|
||||
(void)dict_samples;
|
||||
@@ -138,7 +138,7 @@ class Compressor {
|
||||
// A convenience function when a clone is needed and may or may not be
|
||||
// specialized.
|
||||
std::unique_ptr<Compressor> CloneMaybeSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) {
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
|
||||
auto clone = MaybeCloneSpecialized(block_type, std::move(dict_samples));
|
||||
if (clone == nullptr) {
|
||||
clone = Clone();
|
||||
@@ -496,7 +496,7 @@ class CompressorWrapper : public Compressor {
|
||||
// when the wrapped Compressor uses the default implementation of
|
||||
// MaybeCloneSpecialized(). This needs to be overridden if not.
|
||||
std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) override {
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const override {
|
||||
auto clone =
|
||||
wrapped_->MaybeCloneSpecialized(block_type, std::move(dict_samples));
|
||||
// Assert default no-op MaybeCloneSpecialized()
|
||||
|
||||
@@ -166,6 +166,31 @@ typedef struct rocksdb_writestallinfo_t rocksdb_writestallinfo_t;
|
||||
typedef struct rocksdb_writestallcondition_t rocksdb_writestallcondition_t;
|
||||
typedef struct rocksdb_memtableinfo_t rocksdb_memtableinfo_t;
|
||||
|
||||
// Remote Compaction typedef
|
||||
typedef struct rocksdb_compactionservice_scheduleresponse_t
|
||||
rocksdb_compactionservice_scheduleresponse_t;
|
||||
typedef struct rocksdb_compactionservice_jobinfo_t
|
||||
rocksdb_compactionservice_jobinfo_t;
|
||||
typedef struct rocksdb_compactionservice_t rocksdb_compactionservice_t;
|
||||
typedef struct rocksdb_compaction_service_options_override_t
|
||||
rocksdb_compaction_service_options_override_t;
|
||||
typedef struct rocksdb_open_and_compact_options_t
|
||||
rocksdb_open_and_compact_options_t;
|
||||
typedef rocksdb_compactionservice_scheduleresponse_t* (
|
||||
*rocksdb_compaction_service_schedule_cb)(
|
||||
void* state, const rocksdb_compactionservice_jobinfo_t* info,
|
||||
const char* compaction_service_input, size_t input_len);
|
||||
|
||||
typedef int (*rocksdb_compaction_service_wait_cb)(void* state,
|
||||
const char* scheduled_job_id,
|
||||
char** result,
|
||||
size_t* result_len);
|
||||
|
||||
typedef void (*rocksdb_compaction_service_cancel_awaiting_jobs_cb)(void* state);
|
||||
|
||||
typedef void (*rocksdb_compaction_service_on_installation_cb)(
|
||||
void* state, const char* scheduled_job_id, int status);
|
||||
|
||||
/* DB operations */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_t* rocksdb_open(
|
||||
@@ -3512,6 +3537,155 @@ extern ROCKSDB_LIBRARY_API unsigned char rocksdb_get_into_buffer_cf(
|
||||
size_t keylen, char* buffer, size_t buffer_size, size_t* vallen,
|
||||
unsigned char* found, char** errptr);
|
||||
|
||||
// Remote compaction
|
||||
enum {
|
||||
rocksdb_compactionservice_jobstatus_success = 0,
|
||||
rocksdb_compactionservice_jobstatus_failure = 1,
|
||||
rocksdb_compactionservice_jobstatus_aborted = 2,
|
||||
rocksdb_compactionservice_jobstatus_use_local = 3,
|
||||
};
|
||||
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_compactionservice_scheduleresponse_t*
|
||||
rocksdb_compactionservice_scheduleresponse_create(const char* scheduled_job_id,
|
||||
int status, char** errptr);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_compactionservice_scheduleresponse_t*
|
||||
rocksdb_compactionservice_scheduleresponse_create_with_status(int status,
|
||||
char** errptr);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_compactionservice_scheduleresponse_getstatus(
|
||||
const rocksdb_compactionservice_scheduleresponse_t* response);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_compactionservice_scheduleresponse_get_scheduled_job_id(
|
||||
const rocksdb_compactionservice_scheduleresponse_t* response, size_t* len);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compactionservice_scheduleresponse_t_destroy(
|
||||
rocksdb_compactionservice_scheduleresponse_t* response);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_compactionservice_jobinfo_t_get_db_name(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_compactionservice_jobinfo_t_get_db_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_compactionservice_jobinfo_t_get_db_session_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_compactionservice_jobinfo_t_get_cf_name(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint32_t
|
||||
rocksdb_compactionservice_jobinfo_t_get_cf_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_compactionservice_jobinfo_t_get_job_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int rocksdb_compactionservice_jobinfo_t_get_priority(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_compactionservice_jobinfo_t_get_compaction_reason(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_compactionservice_jobinfo_t_get_base_input_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_compactionservice_jobinfo_t_get_output_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_compactionservice_jobinfo_t_is_full_compaction(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_compactionservice_jobinfo_t_is_manual_compaction(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_compactionservice_jobinfo_t_is_bottommost_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_compactionservice_t*
|
||||
rocksdb_compactionservice_create(
|
||||
void* state, void (*destructor)(void*),
|
||||
rocksdb_compaction_service_schedule_cb schedule, const char* name,
|
||||
rocksdb_compaction_service_wait_cb wait,
|
||||
rocksdb_compaction_service_cancel_awaiting_jobs_cb cancel_awaiting_jobs,
|
||||
rocksdb_compaction_service_on_installation_cb on_installation);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_compaction_service(
|
||||
rocksdb_options_t* options, rocksdb_compactionservice_t* service);
|
||||
|
||||
// CompactionServiceOptionsOverride
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_compaction_service_options_override_t*
|
||||
rocksdb_compaction_service_options_override_create(void);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_destroy(
|
||||
rocksdb_compaction_service_options_override_t* override_options);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_env(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_env_t* env);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_comparator(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_comparator_t* comparator);
|
||||
|
||||
// Atomic bool management for cancellation
|
||||
// Creates an atomic bool that can be used for cancellation.
|
||||
// User must call rocksdb_open_and_compact_canceled_destroy() to free it.
|
||||
extern ROCKSDB_LIBRARY_API unsigned char*
|
||||
rocksdb_open_and_compact_canceled_create(void);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_open_and_compact_canceled_destroy(
|
||||
unsigned char* canceled);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_open_and_compact_canceled_set(
|
||||
unsigned char* canceled, unsigned char value);
|
||||
|
||||
// OpenAndCompactOptions
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_open_and_compact_options_t*
|
||||
rocksdb_open_and_compact_options_create(void);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_open_and_compact_options_destroy(
|
||||
rocksdb_open_and_compact_options_t* options);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_open_and_compact_options_set_canceled(
|
||||
rocksdb_open_and_compact_options_t* options, unsigned char* canceled);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_open_and_compact_options_set_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* options,
|
||||
unsigned char allow_resumption);
|
||||
|
||||
// OpenAndCompact - main functions
|
||||
extern ROCKSDB_LIBRARY_API char* rocksdb_open_and_compact(
|
||||
const char* db_path, const char* output_directory, const char* input,
|
||||
size_t input_len, size_t* output_len,
|
||||
const rocksdb_compaction_service_options_override_t* override_options,
|
||||
char** errptr);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API char* rocksdb_open_and_compact_with_options(
|
||||
const rocksdb_open_and_compact_options_t* options, const char* db_path,
|
||||
const char* output_directory, const char* input, size_t input_len,
|
||||
size_t* output_len,
|
||||
const rocksdb_compaction_service_options_override_t* override_options,
|
||||
char** errptr);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* end extern "C" */
|
||||
#endif
|
||||
@@ -1105,9 +1105,12 @@ class DB {
|
||||
// details. For optimal performance, ensure that either all entries in
|
||||
// scan_opts specify the range limit, or none of them do.
|
||||
//
|
||||
// NOTE: iterate_upper_bound in ReadOptions will be ignored. Instead, the
|
||||
// range.limit in ScanOptions is consulted to determine the upper bound key,
|
||||
// if specified.
|
||||
// NOTE: NOT YET SUPPORTED in DBs using user timestamp (see
|
||||
// Comparator::timestamp_size())
|
||||
//
|
||||
// NOTE: iterate_upper_bound in ReadOptions will
|
||||
// be ignored. Instead, the range.limit in ScanOptions is consulted to
|
||||
// determine the upper bound key, if specified.
|
||||
//
|
||||
// Example usage -
|
||||
// std::vector<ScanOptions> scans{{.start = Slice("bar")},
|
||||
|
||||
@@ -621,7 +621,14 @@ class BackupEngineAppendOnlyBase {
|
||||
// The backup will stop ASAP and the call to CreateNewBackup will
|
||||
// return Status::Incomplete(). It will not clean up after itself, but
|
||||
// the state will remain consistent. The state will be cleaned up the
|
||||
// next time you call CreateNewBackup or GarbageCollect.
|
||||
// next time you call CreateNewBackup or GarbageCollect for the same backup
|
||||
// directory on a new BackupEngine object.
|
||||
//
|
||||
// NOTE: This is a one-way operation. Once StopBackup() is called on a
|
||||
// BackupEngine instance, all subsequent backup requests (CreateNewBackup,
|
||||
// CreateNewBackupWithMetadata) will fail with Status::Incomplete().
|
||||
// To create new backups after calling StopBackup(), you must open a new
|
||||
// BackupEngine instance.
|
||||
virtual void StopBackup() = 0;
|
||||
|
||||
// Will delete any files left over from incomplete creation or deletion of
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
// with `Options::compaction_options_fifo.max_table_files_size` > 0 can cause
|
||||
// the whole DB to be dropped right after migration if the migrated data is
|
||||
// larger than `max_table_files_size`
|
||||
Status OptionChangeMigration(std::string& dbname, const Options& old_opts,
|
||||
Status OptionChangeMigration(const std::string& dbname, const Options& old_opts,
|
||||
const Options& new_opts);
|
||||
|
||||
// Multi-CF version: Prepares a database with multiple column families to be
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
// NOTE: in 'main' development branch, this should be the *next*
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 10
|
||||
#define ROCKSDB_MINOR 9
|
||||
#define ROCKSDB_PATCH 0
|
||||
#define ROCKSDB_MINOR 10
|
||||
#define ROCKSDB_PATCH 1
|
||||
|
||||
// Make it easy to do conditional compilation based on version checks, i.e.
|
||||
// #if ROCKSDB_VERSION_GE(4, 5, 6)
|
||||
|
||||
@@ -652,8 +652,6 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
|
||||
std::to_string(async_state->offset) + " and async callback " +
|
||||
std::to_string(req.offset));
|
||||
}
|
||||
} else {
|
||||
assert(async_state->status.IsAborted());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3232,7 +3232,8 @@ Status BlockBasedTable::GetKVPairsFromDataBlocks(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlockBasedTable::DumpTable(WritableFile* out_file) {
|
||||
Status BlockBasedTable::DumpTable(WritableFile* out_file,
|
||||
bool show_sequence_number_type) {
|
||||
WritableFileStringStreamAdapter out_file_wrapper(out_file);
|
||||
std::ostream out_stream(&out_file_wrapper);
|
||||
// Output Footer
|
||||
@@ -3325,15 +3326,15 @@ Status BlockBasedTable::DumpTable(WritableFile* out_file) {
|
||||
out_stream << "Range deletions:\n"
|
||||
"--------------------------------------\n";
|
||||
for (; range_del_iter->Valid(); range_del_iter->Next()) {
|
||||
DumpKeyValue(range_del_iter->key(), range_del_iter->value(),
|
||||
out_stream);
|
||||
DumpKeyValue(range_del_iter->key(), range_del_iter->value(), out_stream,
|
||||
show_sequence_number_type);
|
||||
}
|
||||
out_stream << "\n";
|
||||
}
|
||||
delete range_del_iter;
|
||||
}
|
||||
// Output Data blocks
|
||||
s = DumpDataBlocks(out_stream);
|
||||
s = DumpDataBlocks(out_stream, show_sequence_number_type);
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -3398,7 +3399,8 @@ Status BlockBasedTable::DumpIndexBlock(std::ostream& out_stream) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlockBasedTable::DumpDataBlocks(std::ostream& out_stream) {
|
||||
Status BlockBasedTable::DumpDataBlocks(std::ostream& out_stream,
|
||||
bool show_sequence_number_type) {
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
const ReadOptions read_options;
|
||||
std::unique_ptr<InternalIteratorBase<IndexValue>> blockhandles_iter(
|
||||
@@ -3455,7 +3457,8 @@ Status BlockBasedTable::DumpDataBlocks(std::ostream& out_stream) {
|
||||
out_stream << "Error reading the block - Skipped \n";
|
||||
break;
|
||||
}
|
||||
DumpKeyValue(datablock_iter->key(), datablock_iter->value(), out_stream);
|
||||
DumpKeyValue(datablock_iter->key(), datablock_iter->value(), out_stream,
|
||||
show_sequence_number_type);
|
||||
}
|
||||
out_stream << "\n";
|
||||
}
|
||||
@@ -3477,14 +3480,26 @@ Status BlockBasedTable::DumpDataBlocks(std::ostream& out_stream) {
|
||||
}
|
||||
|
||||
void BlockBasedTable::DumpKeyValue(const Slice& key, const Slice& value,
|
||||
std::ostream& out_stream) {
|
||||
InternalKey ikey;
|
||||
ikey.DecodeFrom(key);
|
||||
std::ostream& out_stream,
|
||||
bool show_sequence_number_type) {
|
||||
ParsedInternalKey result;
|
||||
auto s = ParseInternalKey(key, &result, true);
|
||||
if (!s.ok()) {
|
||||
out_stream << "Error parsing internal key - Skipped \n";
|
||||
return;
|
||||
}
|
||||
|
||||
out_stream << " HEX " << ikey.user_key().ToString(true) << ": "
|
||||
<< value.ToString(true) << "\n";
|
||||
if (show_sequence_number_type) {
|
||||
out_stream << " HEX " << result.user_key.ToString(true)
|
||||
<< " seq: " << result.sequence
|
||||
<< " type: " << std::to_string(result.type) << " : "
|
||||
<< value.ToString(true) << "\n";
|
||||
} else {
|
||||
out_stream << " HEX " << result.user_key.ToString(true) << ": "
|
||||
<< value.ToString(true) << "\n";
|
||||
}
|
||||
|
||||
std::string str_key = ikey.user_key().ToString();
|
||||
std::string str_key = result.user_key.ToString();
|
||||
std::string str_value = value.ToString();
|
||||
std::string res_key, res_value;
|
||||
char cspace = ' ';
|
||||
|
||||
@@ -208,7 +208,8 @@ class BlockBasedTable : public TableReader {
|
||||
size_t ApproximateMemoryUsage() const override;
|
||||
|
||||
// convert SST file to a human readable form
|
||||
Status DumpTable(WritableFile* out_file) override;
|
||||
Status DumpTable(WritableFile* out_file,
|
||||
bool show_sequence_number_type = false) override;
|
||||
|
||||
Status VerifyChecksum(const ReadOptions& readOptions,
|
||||
TableReaderCaller caller,
|
||||
@@ -549,9 +550,11 @@ class BlockBasedTable : public TableReader {
|
||||
|
||||
// Helper functions for DumpTable()
|
||||
Status DumpIndexBlock(std::ostream& out_stream);
|
||||
Status DumpDataBlocks(std::ostream& out_stream);
|
||||
Status DumpDataBlocks(std::ostream& out_stream,
|
||||
bool show_sequence_number_type = false);
|
||||
void DumpKeyValue(const Slice& key, const Slice& value,
|
||||
std::ostream& out_stream);
|
||||
std::ostream& out_stream,
|
||||
bool show_sequence_number_type = false);
|
||||
|
||||
// Returns false if prefix_extractor exists and is compatible with that used
|
||||
// in building the table file, otherwise true.
|
||||
|
||||
@@ -191,6 +191,13 @@ class CompactionMergingIterator : public InternalIterator {
|
||||
|
||||
bool operator()(HeapItem* a, HeapItem* b) const {
|
||||
int r = comparator_->Compare(a->key(), b->key());
|
||||
// For each file, we assume all range tombstone start keys come before
|
||||
// its file boundary sentinel key (file's meta.largest key).
|
||||
// In the case when meta.smallest = meta.largest and range tombstone start
|
||||
// key is truncated at meta.smallest, the start key will have op_type =
|
||||
// kMaxValid to make it smaller (see TruncatedRangeDelIterator
|
||||
// constructor). The following assertion validates this assumption.
|
||||
assert(a->type == b->type || r != 0);
|
||||
return r > 0;
|
||||
}
|
||||
|
||||
@@ -235,24 +242,8 @@ class CompactionMergingIterator : public InternalIterator {
|
||||
return !minHeap_.empty() ? minHeap_.top() : nullptr;
|
||||
}
|
||||
|
||||
// For each file under a LevelIterator, the lifetime of range tombstone
|
||||
// iterator is tied to the point key iterator. So we want scan through
|
||||
// all range tombstone start keys before the file boundary sentinel key
|
||||
// (file's meta.largest). When meta.smallest == meta.largest, the truncated
|
||||
// range del start key may be ordered after meta.largest.
|
||||
// Here we skip the first range deletion start key if it's truncated.
|
||||
// This range deletion start key is redundant for compaction file cutting
|
||||
// decision anyway, since the same point key will be considered for file
|
||||
// cutting too.
|
||||
void InsertNextValidRangeTombstoneAtLevel(size_t level) {
|
||||
void InsertRangeTombstoneAtLevel(size_t level) {
|
||||
if (range_tombstone_iters_[level]->Valid()) {
|
||||
if (range_tombstone_iters_[level]->start_key().type !=
|
||||
kTypeRangeDeletion) {
|
||||
range_tombstone_iters_[level]->Next();
|
||||
if (!range_tombstone_iters_[level]->Valid()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
pinned_heap_item_[level].SetTombstoneForCompaction(
|
||||
range_tombstone_iters_[level]->start_key());
|
||||
minHeap_.push(&pinned_heap_item_[level]);
|
||||
@@ -271,7 +262,7 @@ void CompactionMergingIterator::SeekToFirst() {
|
||||
for (size_t i = 0; i < range_tombstone_iters_.size(); ++i) {
|
||||
if (range_tombstone_iters_[i]) {
|
||||
range_tombstone_iters_[i]->SeekToFirst();
|
||||
InsertNextValidRangeTombstoneAtLevel(i);
|
||||
InsertRangeTombstoneAtLevel(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +290,7 @@ void CompactionMergingIterator::Seek(const Slice& target) {
|
||||
0) {
|
||||
range_tombstone_iters_[i]->Next();
|
||||
}
|
||||
InsertNextValidRangeTombstoneAtLevel(i);
|
||||
InsertRangeTombstoneAtLevel(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +357,7 @@ void CompactionMergingIterator::FindNextVisibleKey() {
|
||||
minHeap_.pop();
|
||||
}
|
||||
if (range_tombstone_iters_[current->level]) {
|
||||
InsertNextValidRangeTombstoneAtLevel(current->level);
|
||||
InsertRangeTombstoneAtLevel(current->level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,12 +47,13 @@ SstFileDumper::SstFileDumper(const Options& options,
|
||||
Temperature file_temp, size_t readahead_size,
|
||||
bool verify_checksum, bool output_hex,
|
||||
bool decode_blob_index, const EnvOptions& soptions,
|
||||
bool silent)
|
||||
bool silent, bool show_sequence_number_type)
|
||||
: file_name_(file_path),
|
||||
read_num_(0),
|
||||
file_temp_(file_temp),
|
||||
output_hex_(output_hex),
|
||||
decode_blob_index_(decode_blob_index),
|
||||
show_sequence_number_type_(show_sequence_number_type),
|
||||
soptions_(soptions),
|
||||
silent_(silent),
|
||||
options_(options),
|
||||
@@ -220,7 +221,7 @@ Status SstFileDumper::DumpTable(const std::string& out_filename) {
|
||||
Env* env = options_.env;
|
||||
Status s = env->NewWritableFile(out_filename, &out_file, soptions_);
|
||||
if (s.ok()) {
|
||||
s = table_reader_->DumpTable(out_file.get());
|
||||
s = table_reader_->DumpTable(out_file.get(), show_sequence_number_type_);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
// close the file before return error, ignore the close error if there's any
|
||||
|
||||
@@ -21,7 +21,8 @@ class SstFileDumper {
|
||||
bool verify_checksum, bool output_hex,
|
||||
bool decode_blob_index,
|
||||
const EnvOptions& soptions = EnvOptions(),
|
||||
bool silent = false);
|
||||
bool silent = false,
|
||||
bool show_sequence_number_type = false);
|
||||
|
||||
// read_num_limit limits the total number of keys read. If read_num_limit = 0,
|
||||
// then there is no limit. If read_num_limit = 0 or
|
||||
@@ -79,6 +80,7 @@ class SstFileDumper {
|
||||
Temperature file_temp_;
|
||||
bool output_hex_;
|
||||
bool decode_blob_index_;
|
||||
bool show_sequence_number_type_;
|
||||
EnvOptions soptions_;
|
||||
// less verbose in stdout/stderr
|
||||
bool silent_;
|
||||
|
||||
@@ -179,7 +179,8 @@ class TableReader {
|
||||
}
|
||||
|
||||
// convert db file to a human readable form
|
||||
virtual Status DumpTable(WritableFile* /*out_file*/) {
|
||||
virtual Status DumpTable(WritableFile* /*out_file*/,
|
||||
bool /*show_sequence_number_type*/ = false) {
|
||||
return Status::NotSupported("DumpTable() not supported");
|
||||
}
|
||||
|
||||
|
||||
@@ -79,4 +79,8 @@ void SetupSyncPointsToMockDirectIO() {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
std::atomic<int> g_throw_on_testable_assertion_failure{0};
|
||||
#endif // NDEBUG
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -180,3 +180,35 @@ void SetupSyncPointsToMockDirectIO();
|
||||
} \
|
||||
}
|
||||
#endif // NDEBUG
|
||||
|
||||
// An alternative to assert() that is more test-friendly than using
|
||||
// ASSERT_DEATH. Relies on exception propagation.
|
||||
#ifdef NDEBUG
|
||||
#define testable_assert(cond)
|
||||
#else
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
// Intentionally not based on std::exception to reduce places where this
|
||||
// would be caught
|
||||
struct TestableAssertionFailure {};
|
||||
extern std::atomic<int> g_throw_on_testable_assertion_failure;
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
#define testable_assert(cond) \
|
||||
do { \
|
||||
if (ROCKSDB_NAMESPACE::g_throw_on_testable_assertion_failure.load( \
|
||||
std::memory_order_relaxed) > 0) { \
|
||||
if (cond) { \
|
||||
} else \
|
||||
throw ROCKSDB_NAMESPACE::TestableAssertionFailure(); \
|
||||
} else { \
|
||||
assert(cond); \
|
||||
} \
|
||||
} while (0)
|
||||
#define ASSERT_TESTABLE_FAILURE(expr) \
|
||||
do { \
|
||||
ROCKSDB_NAMESPACE::g_throw_on_testable_assertion_failure.fetch_add( \
|
||||
1, std::memory_order_relaxed); \
|
||||
ASSERT_THROW(expr, ROCKSDB_NAMESPACE::TestableAssertionFailure); \
|
||||
ROCKSDB_NAMESPACE::g_throw_on_testable_assertion_failure.fetch_sub( \
|
||||
1, std::memory_order_relaxed); \
|
||||
} while (0)
|
||||
#endif
|
||||
@@ -796,7 +796,7 @@ struct CompressorCustomAlg : public CompressorWrapper {
|
||||
}
|
||||
|
||||
std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) override {
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const override {
|
||||
auto clone =
|
||||
wrapped_->CloneMaybeSpecialized(block_type, std::move(dict_samples));
|
||||
return std::make_unique<CompressorCustomAlg>(std::move(clone));
|
||||
|
||||
@@ -137,7 +137,7 @@ EOF
|
||||
|
||||
# To check for DB forward compatibility with loading options (old version
|
||||
# reading data from new), as well as backward compatibility
|
||||
declare -a db_forward_with_options_refs=("8.6.fb" "8.7.fb" "8.8.fb" "8.9.fb" "8.10.fb" "8.11.fb" "9.0.fb" "9.1.fb" "9.2.fb" "9.3.fb" "9.4.fb" "9.5.fb" "9.6.fb" "9.7.fb" "9.8.fb" "9.9.fb" "9.10.fb" "9.11.fb" "10.0.fb" "10.1.fb" "10.2.fb" "10.3.fb" "10.4.fb" "10.5.fb" "10.6.fb" "10.7.fb" "10.8.fb")
|
||||
declare -a db_forward_with_options_refs=("8.6.fb" "8.7.fb" "8.8.fb" "8.9.fb" "8.10.fb" "8.11.fb" "9.0.fb" "9.1.fb" "9.2.fb" "9.3.fb" "9.4.fb" "9.5.fb" "9.6.fb" "9.7.fb" "9.8.fb" "9.9.fb" "9.10.fb" "9.11.fb" "10.0.fb" "10.1.fb" "10.2.fb" "10.3.fb" "10.4.fb" "10.5.fb" "10.6.fb" "10.7.fb" "10.8.fb" "10.9.fb")
|
||||
# To check for DB forward compatibility without loading options (in addition
|
||||
# to the "with loading options" set), as well as backward compatibility
|
||||
declare -a db_forward_no_options_refs=() # N/A at the moment
|
||||
|
||||
+19
-1
@@ -5,6 +5,7 @@ import argparse
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -22,6 +23,18 @@ def get_random_seed(override):
|
||||
return override
|
||||
|
||||
|
||||
def quote_arg_for_display(arg):
|
||||
"""
|
||||
Quote only the value after '=' for shell display.
|
||||
This makes the printed command safe to copy/paste into a Unix shell.
|
||||
Note: shlex is Unix-focused; Non-Unix shell users may need to adjust quoting after copying.
|
||||
"""
|
||||
if "=" not in arg:
|
||||
return arg
|
||||
flag, value = arg.split("=", 1)
|
||||
return f"{flag}={shlex.quote(value)}"
|
||||
|
||||
|
||||
def setup_random_seed_before_main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
@@ -679,6 +692,8 @@ ts_params = {
|
||||
# Below flag is randomly picked once and kept consistent in following runs.
|
||||
"persist_user_defined_timestamps": random.choice([0, 1, 1]),
|
||||
"use_merge": 0,
|
||||
# Causing failures and not yet compatible
|
||||
"use_multiscan": 0,
|
||||
"use_full_merge_v1": 0,
|
||||
"use_txn": 0,
|
||||
"ingest_external_file_one_in": 0,
|
||||
@@ -1324,7 +1339,10 @@ def gen_cmd(params, unknown_params):
|
||||
|
||||
def execute_cmd(cmd, timeout=None, timeout_pstack=False):
|
||||
child = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
print("Running db_stress with pid=%d: %s\n\n" % (child.pid, " ".join(cmd)))
|
||||
print(
|
||||
"Running db_stress with pid=%d: %s\n\n"
|
||||
% (child.pid, " ".join(quote_arg_for_display(arg) for arg in cmd))
|
||||
)
|
||||
pid = child.pid
|
||||
|
||||
try:
|
||||
|
||||
@@ -63,6 +63,9 @@ void print_help(bool to_stderr) {
|
||||
--decode_blob_index
|
||||
Decode blob indexes and print them in a human-readable format during scans.
|
||||
|
||||
--show_sequence_number_type
|
||||
Show sequence number and value type when executing raw command
|
||||
|
||||
--from=<user_key>
|
||||
Key to start reading from when executing check|scan
|
||||
|
||||
@@ -177,6 +180,7 @@ int SSTDumpTool::Run(int argc, char const* const* argv, Options options) {
|
||||
bool verify_checksum = false;
|
||||
bool output_hex = false;
|
||||
bool decode_blob_index = false;
|
||||
bool show_sequence_number_type = false;
|
||||
bool input_key_hex = false;
|
||||
bool has_from = false;
|
||||
bool has_to = false;
|
||||
@@ -235,6 +239,8 @@ int SSTDumpTool::Run(int argc, char const* const* argv, Options options) {
|
||||
output_hex = true;
|
||||
} else if (strcmp(argv[i], "--decode_blob_index") == 0) {
|
||||
decode_blob_index = true;
|
||||
} else if (strcmp(argv[i], "--show_sequence_number_type") == 0) {
|
||||
show_sequence_number_type = true;
|
||||
} else if (strcmp(argv[i], "--input_key_hex") == 0) {
|
||||
input_key_hex = true;
|
||||
} else if (sscanf(argv[i], "--read_num=%lu%c", (unsigned long*)&n, &junk) ==
|
||||
@@ -531,7 +537,8 @@ int SSTDumpTool::Run(int argc, char const* const* argv, Options options) {
|
||||
|
||||
ROCKSDB_NAMESPACE::SstFileDumper dumper(
|
||||
options, filename, Temperature::kUnknown, readahead_size,
|
||||
verify_checksum, output_hex, decode_blob_index);
|
||||
verify_checksum, output_hex, decode_blob_index, EnvOptions(), false,
|
||||
show_sequence_number_type);
|
||||
|
||||
// Not a valid SST
|
||||
if (!dumper.getStatus().ok()) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
PosixWritableFile now repositions the seek pointer to the new end of file after a call to Truncate.
|
||||
@@ -1 +0,0 @@
|
||||
* Updated standalone range deletion L0 file compaction behavior to avoid compacting with any newer L0 files (which is expensive and not useful).
|
||||
@@ -1 +0,0 @@
|
||||
* Fix a bug where compaction with range deletion can persist kTypeMaxValid in MANIFEST as file metadata. kTypeMaxValid is not supposed to be persisted and can change as new value types are introduced. This can cause a forward compatibility issue where older versions of RocksDB don't recognize kTypeMaxValid from newer versions. A new placeholder value type kTypeTruncatedRangeDeletionSentinel is also introduced to replace kTypeMaxValid when reading existing SST files' metadata from MANIFEST. This allows us to strengthen some checks to avoid using kTypeMaxValid in the future.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed a bug where `DB::GetSortedWalFiles()` could hang when waiting for a purge operation that found nothing to do (potentially triggered by iterator release, flush, compaction, etc.).
|
||||
@@ -1 +0,0 @@
|
||||
Fixed a bug in MultiScan where `max_sequential_skip_in_iterations` could cause the iterator to seek backward to already-unpinned blocks when the same user key spans multiple data blocks, leading to assertion failures or seg fault.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed a bug for `WAL_ttl_seconds > 0` use cases where the newest archived WAL files could be incorrectly deleted when the system clock moved backwards.
|
||||
@@ -1 +0,0 @@
|
||||
Add a new option allow_trivial_move in CompactionOptions to allow CompactFiles to perform trivial move if possible. By default the flag of allow_trivial_move is false, so it preserve the original behavior.
|
||||
@@ -1 +0,0 @@
|
||||
* Added an auto-tuning feature for DB manifest file size that also (by default) improves the safety of existing configurations in case `max_manifest_file_size` is repeatedly exceeded. The new recommendation is to set `max_manifest_file_size` to something small like 1MB and tune `max_manifest_space_amp_pct` as needed to balance write amp and space amp in the manifest. Refer to comments on those options in `DBOptions` for details. Both options are (now) mutable.
|
||||
@@ -1 +0,0 @@
|
||||
Added a new API to support option migration for multiple column families
|
||||
@@ -1 +0,0 @@
|
||||
Added new option target_file_size_is_upper_bound that makes most compaction output SST files come close to the target file size without exceeding it, rather than commonly exceeding it by some fraction (current behavior). For now the new behavior is off by default, but we expect to enable it by default in the future.
|
||||
-1
@@ -1 +0,0 @@
|
||||
Added optimization that allowed for the asynchronous prefetching of all data outlined in a multiscan iterator. This optimization was applied to the level iterator, which prefetches all data through each of the block-based iterators.
|
||||
@@ -1 +0,0 @@
|
||||
* To reduce risk of ODR violations or similar, `ROCKSDB_USING_THREAD_STATUS` has been removed from public headers and replaced with static `const bool ThreadStatus::kEnabled`. Some other uses of conditional compilation have been removed from public API headers to reduce risk of ODR violations or other issues.
|
||||
@@ -63,6 +63,13 @@ std::unique_ptr<Compressor> AutoSkipCompressorWrapper::Clone() const {
|
||||
return std::make_unique<AutoSkipCompressorWrapper>(wrapped_->Clone(), opts_);
|
||||
}
|
||||
|
||||
std::unique_ptr<Compressor> AutoSkipCompressorWrapper::MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
|
||||
auto clone =
|
||||
wrapped_->CloneMaybeSpecialized(block_type, std::move(dict_samples));
|
||||
return std::make_unique<AutoSkipCompressorWrapper>(std::move(clone), opts_);
|
||||
}
|
||||
|
||||
Status AutoSkipCompressorWrapper::CompressBlock(
|
||||
Slice uncompressed_data, char* compressed_output,
|
||||
size_t* compressed_output_size, CompressionType* out_compression_type,
|
||||
@@ -198,7 +205,7 @@ CompressionType CostAwareCompressor::GetPreferredCompressionType() const {
|
||||
return kZSTD;
|
||||
}
|
||||
std::unique_ptr<Compressor> CostAwareCompressor::MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) {
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
|
||||
// TODO: full dictionary compression support. Currently this just falls
|
||||
// back on a non-multi compressor when asked to use a dictionary.
|
||||
auto idx = allcompressors_index_.back();
|
||||
|
||||
@@ -65,6 +65,8 @@ class AutoSkipCompressorWrapper : public CompressorWrapper {
|
||||
const CompressionOptions& opts);
|
||||
|
||||
std::unique_ptr<Compressor> Clone() const override;
|
||||
std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const override;
|
||||
Status CompressBlock(Slice uncompressed_data, char* compressed_output,
|
||||
size_t* compressed_output_size,
|
||||
CompressionType* out_compression_type,
|
||||
@@ -156,7 +158,7 @@ class CostAwareCompressor : public Compressor {
|
||||
CompressionType GetPreferredCompressionType() const override;
|
||||
ManagedWorkingArea ObtainWorkingArea() override;
|
||||
std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) override;
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const override;
|
||||
|
||||
Status CompressBlock(Slice uncompressed_data, char* compressed_output,
|
||||
size_t* compressed_output_size,
|
||||
|
||||
+133
-23
@@ -6,8 +6,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/math.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -144,8 +146,44 @@ struct AndTransform {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: AddTransfrom, which is more complicated due to possible overflow into
|
||||
// other fields etc.
|
||||
// Can represent a combination of both subtractions and additions, representing
|
||||
// subtractions as the addition of a negated value. To ensure we don't create a
|
||||
// net overflow or underflow between fields, in debug builds we track the
|
||||
// corresponding preconditions. (NOTE that when representing a subtraction, we
|
||||
// rely on overflow of the unsigned representation.)
|
||||
template <typename BitFieldsT>
|
||||
struct AddTransform {
|
||||
using U = typename BitFieldsT::U;
|
||||
U to_add = 0;
|
||||
#ifndef NDEBUG
|
||||
struct Precondition {
|
||||
U mask; // for bits of the target field
|
||||
U piece; // component of to_add for the target field
|
||||
};
|
||||
std::vector<Precondition> preconditions;
|
||||
#endif // NDEBUG
|
||||
void AssertPreconditions([[maybe_unused]] U from) {
|
||||
#ifndef NDEBUG
|
||||
for (auto p : preconditions) {
|
||||
U tmp = (from & p.mask) + p.piece;
|
||||
// Assert no under/overflow (unless the field is at the top bits of the
|
||||
// representation in U, which is allowed because it doesn't lead to
|
||||
// leakage into other fields)
|
||||
testable_assert((tmp & ~p.mask) == 0);
|
||||
}
|
||||
#endif // NDEBUG
|
||||
}
|
||||
// + for general combine
|
||||
AddTransform<BitFieldsT> operator+(AddTransform<BitFieldsT> other) const {
|
||||
AddTransform<BitFieldsT> rv{to_add + other.to_add};
|
||||
#ifndef NDEBUG
|
||||
rv.preconditions = preconditions;
|
||||
rv.preconditions.insert(rv.preconditions.end(), other.preconditions.begin(),
|
||||
other.preconditions.end());
|
||||
#endif // NDEBUG
|
||||
return rv;
|
||||
}
|
||||
};
|
||||
|
||||
// Placeholder for PrevField for the first field
|
||||
struct NoPrevBitField {
|
||||
@@ -204,6 +242,7 @@ struct UnsignedBitField {
|
||||
static_assert(kBitCount >= 1);
|
||||
static_assert(kBitCount <= 64);
|
||||
static_assert(kBitOffset >= 0 && kEndBit <= BitFieldsT::kBitCount);
|
||||
static constexpr bool kIncludesTopBit = (kEndBit == BitFieldsT::kBitCount);
|
||||
|
||||
static constexpr V kMask = (V{1} << (kBitCount - 1) << 1) - 1;
|
||||
|
||||
@@ -219,9 +258,59 @@ struct UnsignedBitField {
|
||||
bf.underlying |= static_cast<U>(value & kMask) << kBitOffset;
|
||||
}
|
||||
|
||||
// Create a transfor for clearing this field to zero.
|
||||
static AndTransform<BitFieldsT> ClearTransform() {
|
||||
return AndTransform<BitFieldsT>{~(static_cast<U>(kMask) << kBitOffset)};
|
||||
}
|
||||
|
||||
// Create a transform for adding a particular value, but with the precondition
|
||||
// that adding the value will not overflow the field. This applies for fields
|
||||
// that do not include the top bit of the underlying representation. Can be
|
||||
// combined with other additive transforms for other fields.
|
||||
static AddTransform<BitFieldsT> PlusTransformPromiseNoOverflow(V value) {
|
||||
static_assert(!kIncludesTopBit);
|
||||
AddTransform<BitFieldsT> rv{static_cast<U>(value) << kBitOffset};
|
||||
#ifndef NDEBUG
|
||||
rv.preconditions.push_back(
|
||||
{static_cast<U>(kMask) << kBitOffset, rv.to_add});
|
||||
#endif // NDEBUG
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Create a transform for adding a particular value, but ignoring any overflow
|
||||
// in that field. This applies for fields that include the top bit of the
|
||||
// underlying representation. Can be combined with other additive transforms
|
||||
// for other fields.
|
||||
static AddTransform<BitFieldsT> PlusTransformIgnoreOverflow(V value) {
|
||||
static_assert(kIncludesTopBit);
|
||||
AddTransform<BitFieldsT> rv{static_cast<U>(value) << kBitOffset};
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Create a transform for subtracting a particular value, but with the
|
||||
// precondition that subtracting the value will not underflow the field. This
|
||||
// applies for fields that do not include the top bit of the underlying
|
||||
// representation. Can be combined with other additive transforms for other
|
||||
// fields.
|
||||
static AddTransform<BitFieldsT> MinusTransformPromiseNoUnderflow(V value) {
|
||||
static_assert(!kIncludesTopBit);
|
||||
AddTransform<BitFieldsT> rv{U{0} - (static_cast<U>(value) << kBitOffset)};
|
||||
#ifndef NDEBUG
|
||||
rv.preconditions.push_back(
|
||||
{static_cast<U>(kMask) << kBitOffset, rv.to_add});
|
||||
#endif // NDEBUG
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Create a transform for subtracting a particular value, but ignoring any
|
||||
// underflow in that field. This applies for fields that include the top bit
|
||||
// of the underlying representation. Can be combined with other additive
|
||||
// transforms for other fields.
|
||||
static AddTransform<BitFieldsT> MinusTransformIgnoreUnderflow(V value) {
|
||||
static_assert(kIncludesTopBit);
|
||||
AddTransform<BitFieldsT> rv{U{0} - (static_cast<U>(value) << kBitOffset)};
|
||||
return rv;
|
||||
}
|
||||
};
|
||||
|
||||
// A handy wrapper for a relaxed atomic on some BitFields type (unlike
|
||||
@@ -260,7 +349,22 @@ class RelaxedBitFieldsAtomic {
|
||||
}
|
||||
void ApplyRelaxed(OrTransform<BitFieldsT> transform,
|
||||
BitFieldsT* before = nullptr, BitFieldsT* after = nullptr) {
|
||||
U before_val = v_.fetch_or(transform.to_or, std::memory_order_relaxed);
|
||||
ApplyImpl<std::memory_order_relaxed>(transform, before, after);
|
||||
}
|
||||
void ApplyRelaxed(AndTransform<BitFieldsT> transform,
|
||||
BitFieldsT* before = nullptr, BitFieldsT* after = nullptr) {
|
||||
ApplyImpl<std::memory_order_relaxed>(transform, before, after);
|
||||
}
|
||||
void ApplyRelaxed(AddTransform<BitFieldsT> transform,
|
||||
BitFieldsT* before = nullptr, BitFieldsT* after = nullptr) {
|
||||
ApplyImpl<std::memory_order_relaxed>(transform, before, after);
|
||||
}
|
||||
|
||||
protected: // fns
|
||||
template <std::memory_order kOrder>
|
||||
void ApplyImpl(OrTransform<BitFieldsT> transform,
|
||||
BitFieldsT* before = nullptr, BitFieldsT* after = nullptr) {
|
||||
U before_val = v_.fetch_or(transform.to_or, kOrder);
|
||||
if (before) {
|
||||
before->underlying = before_val;
|
||||
}
|
||||
@@ -268,9 +372,10 @@ class RelaxedBitFieldsAtomic {
|
||||
after->underlying = before_val | transform.to_or;
|
||||
}
|
||||
}
|
||||
void ApplyRelaxed(AndTransform<BitFieldsT> transform,
|
||||
BitFieldsT* before = nullptr, BitFieldsT* after = nullptr) {
|
||||
U before_val = v_.fetch_and(transform.to_and, std::memory_order_relaxed);
|
||||
template <std::memory_order kOrder>
|
||||
void ApplyImpl(AndTransform<BitFieldsT> transform,
|
||||
BitFieldsT* before = nullptr, BitFieldsT* after = nullptr) {
|
||||
U before_val = v_.fetch_and(transform.to_and, kOrder);
|
||||
if (before) {
|
||||
before->underlying = before_val;
|
||||
}
|
||||
@@ -278,8 +383,20 @@ class RelaxedBitFieldsAtomic {
|
||||
after->underlying = before_val & transform.to_and;
|
||||
}
|
||||
}
|
||||
template <std::memory_order kOrder>
|
||||
void ApplyImpl(AddTransform<BitFieldsT> transform,
|
||||
BitFieldsT* before = nullptr, BitFieldsT* after = nullptr) {
|
||||
U before_val = v_.fetch_add(transform.to_add, kOrder);
|
||||
transform.AssertPreconditions(before_val);
|
||||
if (before) {
|
||||
before->underlying = before_val;
|
||||
}
|
||||
if (after) {
|
||||
after->underlying = before_val + transform.to_add;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
protected: // data
|
||||
std::atomic<U> v_;
|
||||
};
|
||||
|
||||
@@ -313,25 +430,18 @@ class AcqRelBitFieldsAtomic : public RelaxedBitFieldsAtomic<BitFieldsT> {
|
||||
}
|
||||
void Apply(OrTransform<BitFieldsT> transform, BitFieldsT* before = nullptr,
|
||||
BitFieldsT* after = nullptr) {
|
||||
U before_val =
|
||||
Base::v_.fetch_or(transform.to_or, std::memory_order_acq_rel);
|
||||
if (before) {
|
||||
before->underlying = before_val;
|
||||
}
|
||||
if (after) {
|
||||
after->underlying = before_val | transform.to_or;
|
||||
}
|
||||
Base::template ApplyImpl<std::memory_order_acq_rel>(transform, before,
|
||||
after);
|
||||
}
|
||||
void Apply(AndTransform<BitFieldsT> transform, BitFieldsT* before = nullptr,
|
||||
BitFieldsT* after = nullptr) {
|
||||
U before_val =
|
||||
Base::v_.fetch_and(transform.to_and, std::memory_order_acq_rel);
|
||||
if (before) {
|
||||
before->underlying = before_val;
|
||||
}
|
||||
if (after) {
|
||||
after->underlying = before_val & transform.to_and;
|
||||
}
|
||||
Base::template ApplyImpl<std::memory_order_acq_rel>(transform, before,
|
||||
after);
|
||||
}
|
||||
void Apply(AddTransform<BitFieldsT> transform, BitFieldsT* before = nullptr,
|
||||
BitFieldsT* after = nullptr) {
|
||||
Base::template ApplyImpl<std::memory_order_acq_rel>(transform, before,
|
||||
after);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+18
-16
@@ -162,7 +162,7 @@ class CompressorBase : public Compressor {
|
||||
CompressionOptions opts_;
|
||||
};
|
||||
|
||||
class BuiltinCompressorV1 : public CompressorBase {
|
||||
class BuiltinCompressorV1 final : public CompressorBase {
|
||||
public:
|
||||
const char* Name() const override { return "BuiltinCompressorV1"; }
|
||||
|
||||
@@ -236,7 +236,7 @@ class CompressorWithSimpleDictBase : public CompressorBase {
|
||||
|
||||
std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole /*block_type*/,
|
||||
DictSampleArgs&& dict_samples) final override {
|
||||
DictSampleArgs&& dict_samples) const final override {
|
||||
assert(dict_samples.Verify());
|
||||
if (dict_samples.empty()) {
|
||||
// Nothing to specialize on
|
||||
@@ -256,7 +256,7 @@ class CompressorWithSimpleDictBase : public CompressorBase {
|
||||
// NOTE: the legacy behavior is to pretend to use dictionary compression when
|
||||
// enabled, including storing a dictionary block, but to ignore it. That is
|
||||
// matched here.
|
||||
class BuiltinSnappyCompressorV2 : public CompressorWithSimpleDictBase {
|
||||
class BuiltinSnappyCompressorV2 final : public CompressorWithSimpleDictBase {
|
||||
public:
|
||||
using CompressorWithSimpleDictBase::CompressorWithSimpleDictBase;
|
||||
|
||||
@@ -349,7 +349,7 @@ std::pair<char*, size_t> StartCompressBlockV2(Slice uncompressed_data,
|
||||
return {alg_output, alg_max_output_size};
|
||||
}
|
||||
|
||||
class BuiltinZlibCompressorV2 : public CompressorWithSimpleDictBase {
|
||||
class BuiltinZlibCompressorV2 final : public CompressorWithSimpleDictBase {
|
||||
public:
|
||||
using CompressorWithSimpleDictBase::CompressorWithSimpleDictBase;
|
||||
|
||||
@@ -448,7 +448,7 @@ class BuiltinZlibCompressorV2 : public CompressorWithSimpleDictBase {
|
||||
}
|
||||
};
|
||||
|
||||
class BuiltinBZip2CompressorV2 : public CompressorWithSimpleDictBase {
|
||||
class BuiltinBZip2CompressorV2 final : public CompressorWithSimpleDictBase {
|
||||
public:
|
||||
using CompressorWithSimpleDictBase::CompressorWithSimpleDictBase;
|
||||
|
||||
@@ -624,7 +624,8 @@ class BuiltinLZ4CompressorV2WithDict : public CompressorWithSimpleDictBase {
|
||||
}
|
||||
};
|
||||
|
||||
class BuiltinLZ4CompressorV2NoDict : public BuiltinLZ4CompressorV2WithDict {
|
||||
class BuiltinLZ4CompressorV2NoDict final
|
||||
: public BuiltinLZ4CompressorV2WithDict {
|
||||
public:
|
||||
BuiltinLZ4CompressorV2NoDict(const CompressionOptions& opts)
|
||||
: BuiltinLZ4CompressorV2WithDict(opts, /*dict_data=*/{}) {}
|
||||
@@ -694,7 +695,7 @@ class BuiltinLZ4CompressorV2NoDict : public BuiltinLZ4CompressorV2WithDict {
|
||||
}
|
||||
};
|
||||
|
||||
class BuiltinLZ4HCCompressorV2 : public CompressorWithSimpleDictBase {
|
||||
class BuiltinLZ4HCCompressorV2 final : public CompressorWithSimpleDictBase {
|
||||
public:
|
||||
using CompressorWithSimpleDictBase::CompressorWithSimpleDictBase;
|
||||
|
||||
@@ -790,7 +791,7 @@ class BuiltinLZ4HCCompressorV2 : public CompressorWithSimpleDictBase {
|
||||
}
|
||||
};
|
||||
|
||||
class BuiltinXpressCompressorV2 : public CompressorWithSimpleDictBase {
|
||||
class BuiltinXpressCompressorV2 final : public CompressorWithSimpleDictBase {
|
||||
public:
|
||||
using CompressorWithSimpleDictBase::CompressorWithSimpleDictBase;
|
||||
|
||||
@@ -840,7 +841,7 @@ class BuiltinXpressCompressorV2 : public CompressorWithSimpleDictBase {
|
||||
}
|
||||
};
|
||||
|
||||
class BuiltinZSTDCompressorV2 : public CompressorBase {
|
||||
class BuiltinZSTDCompressorV2 final : public CompressorBase {
|
||||
public:
|
||||
explicit BuiltinZSTDCompressorV2(const CompressionOptions& opts,
|
||||
CompressionDict&& dict = {})
|
||||
@@ -972,7 +973,8 @@ class BuiltinZSTDCompressorV2 : public CompressorBase {
|
||||
}
|
||||
|
||||
std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole /*block_type*/, DictSampleArgs&& dict_samples) override {
|
||||
CacheEntryRole /*block_type*/,
|
||||
DictSampleArgs&& dict_samples) const override {
|
||||
assert(dict_samples.Verify());
|
||||
if (dict_samples.empty()) {
|
||||
// Nothing to specialize on
|
||||
@@ -1009,7 +1011,7 @@ class BuiltinZSTDCompressorV2 : public CompressorBase {
|
||||
|
||||
// NOTE: this implementation is intentionally SIMPLE based on existing code
|
||||
// and NOT EFFICIENT because this is an old/deprecated format.
|
||||
class BuiltinDecompressorV1 : public Decompressor {
|
||||
class BuiltinDecompressorV1 final : public Decompressor {
|
||||
public:
|
||||
const char* Name() const override { return "BuiltinDecompressorV1"; }
|
||||
|
||||
@@ -1061,7 +1063,7 @@ class BuiltinDecompressorV1 : public Decompressor {
|
||||
}
|
||||
};
|
||||
|
||||
class BuiltinCompressionManagerV1 : public CompressionManager {
|
||||
class BuiltinCompressionManagerV1 final : public CompressionManager {
|
||||
public:
|
||||
BuiltinCompressionManagerV1() = default;
|
||||
~BuiltinCompressionManagerV1() override = default;
|
||||
@@ -1384,7 +1386,7 @@ class BuiltinDecompressorV2 : public Decompressor {
|
||||
}
|
||||
};
|
||||
|
||||
class BuiltinDecompressorV2SnappyOnly : public BuiltinDecompressorV2 {
|
||||
class BuiltinDecompressorV2SnappyOnly final : public BuiltinDecompressorV2 {
|
||||
public:
|
||||
const char* Name() const override {
|
||||
return "BuiltinDecompressorV2SnappyOnly";
|
||||
@@ -1412,7 +1414,7 @@ class BuiltinDecompressorV2SnappyOnly : public BuiltinDecompressorV2 {
|
||||
}
|
||||
};
|
||||
|
||||
class BuiltinDecompressorV2WithDict : public BuiltinDecompressorV2 {
|
||||
class BuiltinDecompressorV2WithDict final : public BuiltinDecompressorV2 {
|
||||
public:
|
||||
explicit BuiltinDecompressorV2WithDict(const Slice& dict) : dict_(dict) {}
|
||||
|
||||
@@ -1505,7 +1507,7 @@ class BuiltinDecompressorV2OptimizeZstd : public BuiltinDecompressorV2 {
|
||||
std::unique_ptr<Decompressor>* /*out*/) override;
|
||||
};
|
||||
|
||||
class BuiltinDecompressorV2OptimizeZstdWithDict
|
||||
class BuiltinDecompressorV2OptimizeZstdWithDict final
|
||||
: public BuiltinDecompressorV2OptimizeZstd {
|
||||
public:
|
||||
explicit BuiltinDecompressorV2OptimizeZstdWithDict(const Slice& dict)
|
||||
@@ -1569,7 +1571,7 @@ Status BuiltinDecompressorV2OptimizeZstd::MaybeCloneForDict(
|
||||
serialized_dict);
|
||||
return Status::OK();
|
||||
}
|
||||
class BuiltinCompressionManagerV2 : public CompressionManager {
|
||||
class BuiltinCompressionManagerV2 final : public CompressionManager {
|
||||
public:
|
||||
BuiltinCompressionManagerV2() = default;
|
||||
~BuiltinCompressionManagerV2() override = default;
|
||||
|
||||
+32
-28
@@ -1364,7 +1364,8 @@ TEST_P(DBCompressionTestMaybeParallel, CompressionManagerWrapper) {
|
||||
}
|
||||
|
||||
std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) override {
|
||||
CacheEntryRole block_type,
|
||||
DictSampleArgs&& dict_samples) const override {
|
||||
std::unique_ptr<Compressor> result = std::make_unique<MyCompressor>(
|
||||
wrapped_->CloneMaybeSpecialized(block_type, std::move(dict_samples)));
|
||||
if (block_type == CacheEntryRole::kDataBlock) {
|
||||
@@ -1991,34 +1992,37 @@ class DBAutoSkip : public DBTestBase {
|
||||
};
|
||||
|
||||
TEST_F(DBAutoSkip, AutoSkipCompressionManager) {
|
||||
for (auto type : GetSupportedCompressions()) {
|
||||
if (type == kNoCompression) {
|
||||
continue;
|
||||
for (uint32_t max_dict_bytes : {0, 10000}) {
|
||||
for (auto type : GetSupportedCompressions()) {
|
||||
if (type == kNoCompression) {
|
||||
continue;
|
||||
}
|
||||
options.compression = type;
|
||||
options.bottommost_compression = type;
|
||||
options.compression_opts.max_dict_bytes = max_dict_bytes;
|
||||
DestroyAndReopen(options);
|
||||
const int kValueSize = 20000;
|
||||
// This will set the rejection ratio to 60%
|
||||
CompressionUnfriendlyPut(6, kValueSize);
|
||||
CompressionFriendlyPut(4, kValueSize);
|
||||
// This will verify all the data block compressions are bypassed based on
|
||||
// previous prediction
|
||||
CompressionUnfriendlyPut(6, kValueSize);
|
||||
CompressionFriendlyPut(4, kValueSize);
|
||||
// This will set the rejection ratio to 40%
|
||||
CompressionUnfriendlyPut(4, kValueSize);
|
||||
CompressionFriendlyPut(6, kValueSize);
|
||||
// This will verify all the data block compression are attempted based on
|
||||
// previous prediction
|
||||
// Compression will be rejected for 6 compression unfriendly blocks
|
||||
// Compression will be accepted for 4 compression friendly blocks
|
||||
CompressionUnfriendlyPut(6, kValueSize);
|
||||
CompressionFriendlyPut(4, kValueSize);
|
||||
// Extra block write to ensure that the all above cases are checked
|
||||
CompressionFriendlyPut(6, kValueSize);
|
||||
CompressionFriendlyPut(4, kValueSize);
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
options.compression = type;
|
||||
options.bottommost_compression = type;
|
||||
DestroyAndReopen(options);
|
||||
const int kValueSize = 20000;
|
||||
// This will set the rejection ratio to 60%
|
||||
CompressionUnfriendlyPut(6, kValueSize);
|
||||
CompressionFriendlyPut(4, kValueSize);
|
||||
// This will verify all the data block compressions are bypassed based on
|
||||
// previous prediction
|
||||
CompressionUnfriendlyPut(6, kValueSize);
|
||||
CompressionFriendlyPut(4, kValueSize);
|
||||
// This will set the rejection ratio to 40%
|
||||
CompressionUnfriendlyPut(4, kValueSize);
|
||||
CompressionFriendlyPut(6, kValueSize);
|
||||
// This will verify all the data block compression are attempted based on
|
||||
// previous prediction
|
||||
// Compression will be rejected for 6 compression unfriendly blocks
|
||||
// Compression will be accepted for 4 compression friendly blocks
|
||||
CompressionUnfriendlyPut(6, kValueSize);
|
||||
CompressionFriendlyPut(4, kValueSize);
|
||||
// Extra block write to ensure that the all above cases are checked
|
||||
CompressionFriendlyPut(6, kValueSize);
|
||||
CompressionFriendlyPut(4, kValueSize);
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
}
|
||||
class CostAwareTestFlushBlockPolicy : public FlushBlockPolicy {
|
||||
|
||||
@@ -48,7 +48,7 @@ Compressor::ManagedWorkingArea MultiCompressorWrapper::ObtainWorkingArea() {
|
||||
}
|
||||
|
||||
std::unique_ptr<Compressor> MultiCompressorWrapper::MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) {
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
|
||||
// TODO: full dictionary compression support. Currently this just falls
|
||||
// back on a non-multi compressor when asked to use a dictionary.
|
||||
return compressors_.back()->MaybeCloneSpecialized(block_type,
|
||||
|
||||
@@ -25,7 +25,7 @@ class MultiCompressorWrapper : public Compressor {
|
||||
CompressionType GetPreferredCompressionType() const override;
|
||||
ManagedWorkingArea ObtainWorkingArea() override;
|
||||
std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) override;
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const override;
|
||||
|
||||
protected:
|
||||
const CompressionOptions opts_;
|
||||
|
||||
@@ -527,6 +527,67 @@ TEST(BitFieldsTest, BitFields) {
|
||||
ASSERT_EQ(before.Get<Field2>(), false);
|
||||
ASSERT_EQ(before.Get<Field3>(), false);
|
||||
ASSERT_EQ(after, state);
|
||||
|
||||
ASSERT_EQ(state.Get<Field1>(), 45U);
|
||||
ASSERT_EQ(after.Get<Field2>(), true);
|
||||
ASSERT_EQ(after.Get<Field3>(), true);
|
||||
ASSERT_EQ(state.Get<Field4>(), 3U);
|
||||
|
||||
auto transform3 = Field1::PlusTransformPromiseNoOverflow(10000U) +
|
||||
Field4::MinusTransformPromiseNoUnderflow(3U);
|
||||
relaxed.ApplyRelaxed(transform3, &before, &after);
|
||||
ASSERT_EQ(before, state);
|
||||
ASSERT_NE(after, state);
|
||||
ASSERT_EQ(after.Get<Field1>(), 10045U);
|
||||
ASSERT_EQ(after.Get<Field4>(), 0U);
|
||||
|
||||
auto transform4 = Field1::MinusTransformPromiseNoUnderflow(999U) +
|
||||
Field4::PlusTransformPromiseNoOverflow(31U);
|
||||
relaxed.ApplyRelaxed(transform4, &before, &after);
|
||||
ASSERT_EQ(after.Get<Field1>(), 9046U);
|
||||
ASSERT_EQ(after.Get<Field4>(), 31U);
|
||||
|
||||
// Unmodified
|
||||
ASSERT_EQ(after.Get<Field2>(), true);
|
||||
ASSERT_EQ(after.Get<Field3>(), true);
|
||||
|
||||
// Test overflow/underflow detection
|
||||
relaxed.StoreRelaxed(MyState{}.With<Field1>(65535U)); // Field1 max value
|
||||
ASSERT_TESTABLE_FAILURE(
|
||||
relaxed.ApplyRelaxed(Field1::PlusTransformPromiseNoOverflow(1U)));
|
||||
relaxed.StoreRelaxed(MyState{}.With<Field4>(31U)); // Field4 max value
|
||||
ASSERT_TESTABLE_FAILURE(
|
||||
relaxed.ApplyRelaxed(Field4::PlusTransformPromiseNoOverflow(1U)));
|
||||
relaxed.StoreRelaxed(MyState{}.With<Field1>(0U));
|
||||
ASSERT_TESTABLE_FAILURE(
|
||||
relaxed.ApplyRelaxed(Field1::MinusTransformPromiseNoUnderflow(1U)));
|
||||
relaxed.StoreRelaxed(MyState{}.With<Field4>(0U));
|
||||
ASSERT_TESTABLE_FAILURE(
|
||||
relaxed.ApplyRelaxed(Field4::MinusTransformPromiseNoUnderflow(1U)));
|
||||
ASSERT_TESTABLE_FAILURE(relaxed.ApplyRelaxed(
|
||||
Field4::MinusTransformPromiseNoUnderflow(64U))); // Too big
|
||||
ASSERT_TESTABLE_FAILURE(relaxed.ApplyRelaxed(
|
||||
Field4::PlusTransformPromiseNoOverflow(64U))); // Too big
|
||||
|
||||
// Including combinations
|
||||
relaxed.StoreRelaxed(MyState{}.With<Field4>(31U)); // Field4 max value
|
||||
relaxed.StoreRelaxed(MyState{}.With<Field1>(0U));
|
||||
ASSERT_TESTABLE_FAILURE(
|
||||
relaxed.ApplyRelaxed(Field4::PlusTransformPromiseNoOverflow(1U) +
|
||||
Field1::MinusTransformPromiseNoUnderflow(1U)));
|
||||
|
||||
// But a field at the limit of upper bits is allowed to over/underflow
|
||||
using Field5 = UnsignedBitField<MyState, 9, Field4>;
|
||||
relaxed.StoreRelaxed(MyState{}.With<Field5>(0)); // Field5 max value
|
||||
relaxed.ApplyRelaxed(Field5::MinusTransformIgnoreUnderflow(1U), &before,
|
||||
&after); // "Safe" underflow
|
||||
ASSERT_EQ(after.Get<Field5>(), 511U);
|
||||
relaxed.ApplyRelaxed(Field5::PlusTransformIgnoreOverflow(1U), &before,
|
||||
&after); // "Safe" overflow
|
||||
ASSERT_EQ(after.Get<Field5>(), 0U);
|
||||
relaxed.ApplyRelaxed(Field5::PlusTransformIgnoreOverflow(2048U), &before,
|
||||
&after); // "Safe" overflow
|
||||
ASSERT_EQ(after.Get<Field5>(), 0U);
|
||||
}
|
||||
{
|
||||
AcqRelBitFieldsAtomic<MyState> acqrel{state};
|
||||
@@ -555,6 +616,37 @@ TEST(BitFieldsTest, BitFields) {
|
||||
ASSERT_EQ(before.Get<Field2>(), false);
|
||||
ASSERT_EQ(before.Get<Field3>(), false);
|
||||
ASSERT_EQ(after, state);
|
||||
|
||||
ASSERT_EQ(state.Get<Field1>(), 45U);
|
||||
ASSERT_EQ(after.Get<Field2>(), true);
|
||||
ASSERT_EQ(after.Get<Field3>(), true);
|
||||
ASSERT_EQ(state.Get<Field4>(), 3U);
|
||||
|
||||
auto transform3 = Field1::PlusTransformPromiseNoOverflow(10000U) +
|
||||
Field4::MinusTransformPromiseNoUnderflow(3U);
|
||||
acqrel.Apply(transform3, &before, &after);
|
||||
ASSERT_EQ(before, state);
|
||||
ASSERT_NE(after, state);
|
||||
ASSERT_EQ(after.Get<Field1>(), 10045U);
|
||||
ASSERT_EQ(after.Get<Field4>(), 0U);
|
||||
|
||||
auto transform4 = Field1::MinusTransformPromiseNoUnderflow(999U) +
|
||||
Field4::PlusTransformPromiseNoOverflow(31U);
|
||||
acqrel.Apply(transform4, &before, &after);
|
||||
ASSERT_EQ(after.Get<Field1>(), 9046U);
|
||||
ASSERT_EQ(after.Get<Field4>(), 31U);
|
||||
|
||||
// Unmodified
|
||||
ASSERT_EQ(after.Get<Field2>(), true);
|
||||
ASSERT_EQ(after.Get<Field3>(), true);
|
||||
|
||||
// Test overflow/underflow detection
|
||||
acqrel.Store(MyState{}.With<Field1>(65535U));
|
||||
ASSERT_TESTABLE_FAILURE(
|
||||
acqrel.Apply(Field1::PlusTransformPromiseNoOverflow(1U)));
|
||||
acqrel.Store(MyState{}.With<Field4>(0U));
|
||||
ASSERT_TESTABLE_FAILURE(
|
||||
acqrel.Apply(Field4::MinusTransformPromiseNoUnderflow(1U)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -615,6 +615,10 @@ class BackupEngineImpl {
|
||||
std::string* checksum_hex,
|
||||
const Temperature src_temperature) const;
|
||||
|
||||
// Helper method to check if backup should be stopped. Can be overridden
|
||||
// via sync points for testing.
|
||||
bool ShouldStopBackup() const;
|
||||
|
||||
// Obtain db_id and db_session_id from the table properties of file_path
|
||||
Status GetFileDbIdentities(Env* src_env, const EnvOptions& src_env_options,
|
||||
const std::string& file_path,
|
||||
@@ -2353,6 +2357,10 @@ IOStatus BackupEngineImpl::CopyOrCreateFile(
|
||||
Temperature dst_temperature, uint64_t* bytes_toward_next_callback,
|
||||
uint64_t* size, std::string* checksum_hex) {
|
||||
assert(src.empty() != contents.empty());
|
||||
if (ShouldStopBackup()) {
|
||||
return status_to_io_status(Status::Incomplete("Backup stopped"));
|
||||
}
|
||||
|
||||
IOStatus io_s;
|
||||
std::unique_ptr<FSWritableFile> dst_file;
|
||||
std::unique_ptr<FSSequentialFile> src_file;
|
||||
@@ -2413,7 +2421,7 @@ IOStatus BackupEngineImpl::CopyOrCreateFile(
|
||||
Slice data;
|
||||
const IOOptions opts;
|
||||
do {
|
||||
if (stop_backup_.load(std::memory_order_acquire)) {
|
||||
if (ShouldStopBackup()) {
|
||||
return status_to_io_status(Status::Incomplete("Backup stopped"));
|
||||
}
|
||||
if (!src.empty()) {
|
||||
@@ -2749,6 +2757,12 @@ IOStatus BackupEngineImpl::AddBackupFileWorkItem(
|
||||
return IOStatus::OK();
|
||||
}
|
||||
|
||||
bool BackupEngineImpl::ShouldStopBackup() const {
|
||||
bool should_stop = stop_backup_.load(std::memory_order_acquire);
|
||||
TEST_SYNC_POINT_CALLBACK("BackupEngineImpl::ShouldStopBackup", &should_stop);
|
||||
return should_stop;
|
||||
}
|
||||
|
||||
IOStatus BackupEngineImpl::ReadFileAndComputeChecksum(
|
||||
const std::string& src, const std::shared_ptr<FileSystem>& src_fs,
|
||||
const EnvOptions& src_env_options, uint64_t size_limit,
|
||||
@@ -2756,6 +2770,9 @@ IOStatus BackupEngineImpl::ReadFileAndComputeChecksum(
|
||||
if (checksum_hex == nullptr) {
|
||||
return status_to_io_status(Status::Aborted("Checksum pointer is null"));
|
||||
}
|
||||
if (ShouldStopBackup()) {
|
||||
return status_to_io_status(Status::Incomplete("Backup stopped"));
|
||||
}
|
||||
uint32_t checksum_value = 0;
|
||||
if (size_limit == 0) {
|
||||
size_limit = std::numeric_limits<uint64_t>::max();
|
||||
@@ -2783,7 +2800,7 @@ IOStatus BackupEngineImpl::ReadFileAndComputeChecksum(
|
||||
Slice data;
|
||||
|
||||
do {
|
||||
if (stop_backup_.load(std::memory_order_acquire)) {
|
||||
if (ShouldStopBackup()) {
|
||||
return status_to_io_status(Status::Incomplete("Backup stopped"));
|
||||
}
|
||||
size_t buffer_to_read =
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/atomic.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/mutexlock.h"
|
||||
#include "util/random.h"
|
||||
@@ -4790,6 +4791,79 @@ TEST_F(BackupEngineTest, IOBufferSize) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
// Test stopping backup at different points in the backup lifecycle
|
||||
// Uses randomized stop points with geometric distribution to better catch
|
||||
// edge cases across multiple iterations.
|
||||
TEST_F(BackupEngineTest, StopBackupAtDifferentStages) {
|
||||
const int keys_iteration = 5000;
|
||||
const int num_iterations = 10;
|
||||
|
||||
// Enable multi-threaded backup
|
||||
engine_options_->max_background_operations = 7;
|
||||
|
||||
// Generate DB once and reuse across iterations
|
||||
OpenDBAndBackupEngine(true);
|
||||
FillDB(db_.get(), 0, keys_iteration);
|
||||
|
||||
Random rnd(301);
|
||||
|
||||
for (int iteration = 0; iteration < num_iterations; iteration++) {
|
||||
// Generate stop threshold using skewed distribution
|
||||
// Smaller numbers are more likely, which is more interesting for testing
|
||||
// Range: [0, 2^7-1] = [0, 127] with exponential bias towards 0
|
||||
int stop_after_calls = rnd.Skewed(7);
|
||||
|
||||
RelaxedAtomic<int> call_count{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackupEngineImpl::ShouldStopBackup", [&](void* arg) {
|
||||
call_count.FetchAddRelaxed(1);
|
||||
if (call_count.LoadRelaxed() > stop_after_calls) {
|
||||
bool* should_stop = static_cast<bool*>(arg);
|
||||
*should_stop = true;
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Create backup - it may complete successfully or be stopped
|
||||
IOStatus s = backup_engine_->CreateNewBackup(db_.get());
|
||||
|
||||
// Verify that ShouldStopBackup was called
|
||||
ASSERT_GT(call_count.LoadRelaxed(), 0);
|
||||
|
||||
if (s.IsIncomplete()) {
|
||||
// Backup was stopped - verify it's the expected error
|
||||
ASSERT_TRUE(s.ToString().find("Backup stopped") != std::string::npos)
|
||||
<< "Unexpected incomplete status for threshold " << stop_after_calls
|
||||
<< ": " << s.ToString();
|
||||
ASSERT_GT(call_count.LoadRelaxed(), stop_after_calls)
|
||||
<< "Expected call_count > stop_after_calls";
|
||||
|
||||
// Verify that no valid backup was created
|
||||
std::vector<BackupInfo> backup_info;
|
||||
backup_engine_->GetBackupInfo(&backup_info);
|
||||
ASSERT_EQ(0, backup_info.size());
|
||||
} else {
|
||||
// Backup completed successfully before reaching the stop threshold
|
||||
ASSERT_OK(s) << "Unexpected error for threshold " << stop_after_calls;
|
||||
ASSERT_LE(call_count.LoadRelaxed(), stop_after_calls)
|
||||
<< "Backup completed but call_count exceeded threshold";
|
||||
|
||||
// Verify a backup was created
|
||||
std::vector<BackupInfo> backup_info;
|
||||
backup_engine_->GetBackupInfo(&backup_info);
|
||||
ASSERT_EQ(1, backup_info.size());
|
||||
|
||||
// Clean up the successful backup for next iteration
|
||||
ASSERT_OK(backup_engine_->DeleteBackup(backup_info[0].backup_id));
|
||||
}
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
CloseDBAndBackupEngine();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -41,8 +41,6 @@ static std::unordered_map<std::string, ValueType> value_type_string_map = {
|
||||
{"TypeValuePreferredSeqno", ValueType::kTypeValuePreferredSeqno},
|
||||
{"TypeColumnFamilyValuePreferredSeqno",
|
||||
ValueType::kTypeColumnFamilyValuePreferredSeqno},
|
||||
{"kTypeTruncatedRangeDeletionSentinel",
|
||||
ValueType::kTypeTruncatedRangeDeletionSentinel},
|
||||
};
|
||||
|
||||
std::string KeyVersion::GetTypeName() const {
|
||||
|
||||
@@ -341,7 +341,7 @@ Status OptionChangeMigration(
|
||||
return s;
|
||||
}
|
||||
|
||||
Status OptionChangeMigration(std::string& dbname, const Options& old_opts,
|
||||
Status OptionChangeMigration(const std::string& dbname, const Options& old_opts,
|
||||
const Options& new_opts) {
|
||||
DBOptions old_db_opts(old_opts);
|
||||
DBOptions new_db_opts(new_opts);
|
||||
|
||||
Reference in new issue
Block a user