mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 14:47:40 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c9b3a8e99 | |||
| 1ea163bdcf | |||
| ed5555ba44 | |||
| 08d8122809 | |||
| 7be9ffc81a | |||
| 1d37a88852 | |||
| 7b0019a1b8 | |||
| d3a28293a5 | |||
| 2b3cdc5764 | |||
| 6c65d8046d | |||
| 2126b6e3ba |
@@ -378,10 +378,10 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"utilities/write_batch_with_index/write_batch_with_index_internal.cc",
|
||||
], deps=[
|
||||
"//folly/container:f14_hash",
|
||||
"//folly/coro:blocking_wait",
|
||||
"//folly/coro:collect",
|
||||
"//folly/coro:coroutine",
|
||||
"//folly/coro:task",
|
||||
"//folly/experimental/coro:blocking_wait",
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
], headers=glob(["**/*.h"]), link_whole=False, extra_test_libs=False)
|
||||
|
||||
|
||||
+25
@@ -1,6 +1,31 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.2.2 (05/07/2026)
|
||||
## 11.2.1 (04/29/2026)
|
||||
### Bug Fixes
|
||||
* Fix bug in range tombstone synthesis that covers live keys added during an IngestExternalFile
|
||||
|
||||
## 11.2.0 (04/18/2026)
|
||||
### New Features
|
||||
* Added experimental `DBOptions::fast_sst_open` option. When enabled, RocksDB retrieves opaque file system metadata for SST files after flush, compaction, and external file ingestion, persists it in the MANIFEST, and passes it back to the file system on subsequent file opens to accelerate DB open time.
|
||||
* Added new option `min_tombstones_for_range_conversion` in `AdvancedColumnFamilyOptions`. When set to a non-zero value N, forward or reverse iteration will convert N or more contiguous point tombstones into a range tombstone in the mutable memtable. Future read operations will then be able to benefit from range tombstone optimizations. There are some limitations when it comes to table_filters, prefix_filters, and UDTs. See header comments for more details.
|
||||
* Added read-triggered compaction: a new column family option `read_triggered_compaction_threshold` (default 0, disabled) that marks SST files for compaction when their read frequency (`num_collapsible_entry_reads_sampled / file_size`) exceeds the threshold. This helps reduce read amplification for frequently-read ("hot") keys. A new DB option `max_compaction_trigger_wakeup_seconds` (default 43200s / 12 hours) controls the maximum interval for periodic compaction score re-evaluation, which is necessary for this feature to work on quiet (no-write) databases.
|
||||
|
||||
### Public API Changes
|
||||
* Added new `WideColumnBlobResolver` interface and `CompactionFilter::FilterV4()` method, including `ResolveColumn()` / `ResolveColumns()` helpers for lazy loading blob column values in wide-column entities during compaction. This allows compaction filters to resolve blob values on-demand, avoiding unnecessary I/O for blob columns they don't need to access.
|
||||
* Changed experimental feature `ExternalTableReader::Get` and `ExternalTableReader::MultiGet` to use `PinnableSlice` instead of `std::string` for output values, enabling zero-copy pinning. This will break existing implementations.
|
||||
* Added `SstFileReader::Get` and `SstFileReader::MultiGet` overloads that accept `PinnableSlice`/`std::vector<PinnableSlice>*`, enabling zero-copy reads when the underlying `TableReader` supports pinning.
|
||||
|
||||
### Behavior Changes
|
||||
* Prefix filter changes - when seeking to a key that is out of domain, and total_order_seek is false, total_order_seek is treated as if it were true. When prefix_same_as_start = true, now iterating past a key that is out of domain invalidates the iterator. The existing behavior does not check for InDomain in the DBIter, so Transform() can produce undefined behavior (e.g. key of size 3 on FixedPrefixTransform(4)).
|
||||
* Wide-column entities with blob-backed columns now use a new V2 on-disk encoding; older RocksDB versions that do not support wide-column blob separation will reject DBs or SSTs containing those entities.
|
||||
|
||||
### Bug Fixes
|
||||
* Fix blob garbage accounting for blob direct-write flushes so flush-time filtering and overwrite elision correctly register obsolete blob bytes in blob metadata.
|
||||
* Fix a memory accounting leak in IODispatcher where ReadIndex() moved block values out of ReadSet without releasing the associated prefetch memory, causing subsequent prefetches to be blocked when max_prefetch_memory_bytes was set.
|
||||
* Fix MultiScan to fall back to synchronous coalesced reads when async I/O is unsupported at runtime.
|
||||
|
||||
## 11.1.0 (03/23/2026)
|
||||
### New Features
|
||||
* Add a new option `open_files_async`. The existing behavior is on DB open, we open all sst files and do basic validations. For very large DBs on remote filesystems with many ssts, this may take very long. This option performs these validations instead in the background. Open errors found by this async background task are surfaced as a new background error kAsyncFileOpen.
|
||||
|
||||
@@ -146,10 +146,10 @@ def generate_buck(repo_path, deps_map):
|
||||
src_mk["RANGE_TREE_SOURCES"] + src_mk["TOOL_LIB_SOURCES"],
|
||||
deps=[
|
||||
"//folly/container:f14_hash",
|
||||
"//folly/coro:blocking_wait",
|
||||
"//folly/coro:collect",
|
||||
"//folly/coro:coroutine",
|
||||
"//folly/coro:task",
|
||||
"//folly/experimental/coro:blocking_wait",
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
],
|
||||
headers=LiteralValue("glob([\"**/*.h\"])")
|
||||
|
||||
+13
-14
@@ -612,7 +612,7 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
const FileOptions* file_options, ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id, bool read_only)
|
||||
const std::string& db_session_id, bool read_only, bool fast_sst_open)
|
||||
: id_(id),
|
||||
name_(name),
|
||||
dummy_versions_(_dummy_versions),
|
||||
@@ -672,7 +672,7 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
new InternalStats(ioptions_.num_levels, ioptions_.clock, this));
|
||||
table_cache_.reset(new TableCache(ioptions_, file_options, _table_cache,
|
||||
block_cache_tracer, io_tracer,
|
||||
db_session_id));
|
||||
db_session_id, fast_sst_open));
|
||||
blob_file_cache_.reset(
|
||||
new BlobFileCache(_table_cache, &ioptions(), soptions(), id_,
|
||||
internal_stats_->GetBlobFileReadHist(), io_tracer));
|
||||
@@ -1809,16 +1809,14 @@ void ColumnFamilyData::RecoverEpochNumbers() {
|
||||
vstorage->RecoverEpochNumbers(this);
|
||||
}
|
||||
|
||||
ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
|
||||
const ImmutableDBOptions* db_options,
|
||||
const FileOptions& file_options,
|
||||
Cache* table_cache,
|
||||
WriteBufferManager* _write_buffer_manager,
|
||||
WriteController* _write_controller,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id,
|
||||
const std::string& db_session_id)
|
||||
ColumnFamilySet::ColumnFamilySet(
|
||||
const std::string& dbname, const ImmutableDBOptions* db_options,
|
||||
const FileOptions& file_options, Cache* table_cache,
|
||||
WriteBufferManager* _write_buffer_manager,
|
||||
WriteController* _write_controller,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id, bool fast_sst_open)
|
||||
: max_column_family_(0),
|
||||
file_options_(file_options),
|
||||
dummy_cfd_(new ColumnFamilyData(
|
||||
@@ -1835,7 +1833,8 @@ ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
|
||||
block_cache_tracer_(block_cache_tracer),
|
||||
io_tracer_(io_tracer),
|
||||
db_id_(db_id),
|
||||
db_session_id_(db_session_id) {
|
||||
db_session_id_(db_session_id),
|
||||
fast_sst_open_(fast_sst_open) {
|
||||
// initialize linked list
|
||||
dummy_cfd_->prev_ = dummy_cfd_;
|
||||
dummy_cfd_->next_ = dummy_cfd_;
|
||||
@@ -1902,7 +1901,7 @@ ColumnFamilyData* ColumnFamilySet::CreateColumnFamily(
|
||||
ColumnFamilyData* new_cfd = new ColumnFamilyData(
|
||||
id, name, dummy_versions, table_cache_, write_buffer_manager_, options,
|
||||
*db_options_, &file_options_, this, block_cache_tracer_, io_tracer_,
|
||||
db_id_, db_session_id_, read_only);
|
||||
db_id_, db_session_id_, read_only, fast_sst_open_);
|
||||
column_families_.insert({name, id});
|
||||
column_family_data_.insert({id, new_cfd});
|
||||
auto ucmp = new_cfd->user_comparator();
|
||||
|
||||
+25
-9
@@ -619,16 +619,23 @@ class ColumnFamilyData {
|
||||
return ioptions_.cf_allow_ingest_behind || ioptions_.allow_ingest_behind;
|
||||
}
|
||||
|
||||
// Per-CF reader-writer lock that serializes IngestExternalFiles with range
|
||||
// tombstone conversion.
|
||||
port::RWMutex& GetIngestSstLock() { return ingest_sst_lock_; }
|
||||
|
||||
private:
|
||||
friend class ColumnFamilySet;
|
||||
ColumnFamilyData(
|
||||
uint32_t id, const std::string& name, Version* dummy_versions,
|
||||
Cache* table_cache, WriteBufferManager* write_buffer_manager,
|
||||
const ColumnFamilyOptions& options, const ImmutableDBOptions& db_options,
|
||||
const FileOptions* file_options, ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id, bool read_only);
|
||||
ColumnFamilyData(uint32_t id, const std::string& name,
|
||||
Version* dummy_versions, Cache* table_cache,
|
||||
WriteBufferManager* write_buffer_manager,
|
||||
const ColumnFamilyOptions& options,
|
||||
const ImmutableDBOptions& db_options,
|
||||
const FileOptions* file_options,
|
||||
ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
bool read_only, bool fast_sst_open = false);
|
||||
|
||||
std::vector<std::string> GetDbPaths() const;
|
||||
|
||||
@@ -729,6 +736,10 @@ class ColumnFamilyData {
|
||||
bool mempurge_used_;
|
||||
|
||||
std::atomic<uint64_t> next_epoch_number_;
|
||||
|
||||
// Used to synchronize IngestExternalFile with range tombstone conversion. See
|
||||
// also Memtable::ingest_seqno_barrier_.
|
||||
port::RWMutex ingest_sst_lock_;
|
||||
};
|
||||
|
||||
// ColumnFamilySet has interesting thread-safety requirements
|
||||
@@ -774,7 +785,8 @@ class ColumnFamilySet {
|
||||
WriteController* _write_controller,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id, const std::string& db_session_id);
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
bool fast_sst_open = false);
|
||||
~ColumnFamilySet();
|
||||
|
||||
ColumnFamilyData* GetDefault() const;
|
||||
@@ -810,6 +822,9 @@ class ColumnFamilySet {
|
||||
|
||||
Cache* get_table_cache() { return table_cache_; }
|
||||
|
||||
bool GetFastSstOpen() const { return fast_sst_open_; }
|
||||
void SetFastSstOpen(bool v) { fast_sst_open_ = v; }
|
||||
|
||||
WriteBufferManager* write_buffer_manager() { return write_buffer_manager_; }
|
||||
|
||||
WriteController* write_controller() { return write_controller_; }
|
||||
@@ -858,6 +873,7 @@ class ColumnFamilySet {
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
const std::string& db_id_;
|
||||
std::string db_session_id_;
|
||||
bool fast_sst_open_;
|
||||
};
|
||||
|
||||
// A wrapper for ColumnFamilySet that supports releasing DB mutex during each
|
||||
|
||||
@@ -846,11 +846,16 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct InternalStats::CompactionStats, count),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"counts", OptionTypeInfo::Array<
|
||||
int, static_cast<int>(CompactionReason::kNumOfReasons)>(
|
||||
offsetof(struct InternalStats::CompactionStats, counts),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kInt})},
|
||||
{"counts",
|
||||
OptionTypeInfo::Array<
|
||||
/* In release 11.2, a new compaction reason was added. This broken
|
||||
* the reader and writer. To unblock release 11.1, we temporarily
|
||||
* reduce the count array size to the old one. TODO add a proper
|
||||
* serialization and deserialization method. */
|
||||
int, static_cast<int>(CompactionReason::kNumOfReasons) - 1>(
|
||||
offsetof(struct InternalStats::CompactionStats, counts),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kInt})},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
|
||||
+6
-2
@@ -3946,8 +3946,12 @@ TEST_F(DBFlushTest, FlushAfterReadPathRangeTombstoneInsertion) {
|
||||
// MarkImmutable), this returns false because the memtable is
|
||||
// already immutable. Without the fix, this succeeds and creates
|
||||
// an entry count mismatch.
|
||||
cfh->cfd()->mem()->AddLogicallyRedundantRangeTombstone(1 /* seq */, "b",
|
||||
"f");
|
||||
// Try to insert a range tombstone. With the fix (Construct after
|
||||
// MarkImmutable), this returns false because the memtable is
|
||||
// already immutable. Without the fix, this succeeds and creates
|
||||
// an entry count mismatch.
|
||||
cfh->cfd()->mem()->AddLogicallyRedundantRangeTombstone(
|
||||
1 /* seq */, "b", "f", cfh->cfd()->GetIngestSstLock());
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
|
||||
@@ -1665,9 +1665,12 @@ Status DBImpl::SetDBOptions(
|
||||
: new_options.max_open_files - 10);
|
||||
// Potential table cache capacity change requires updating if table
|
||||
// handles should get pinned.
|
||||
versions_->GetColumnFamilySet()->SetFastSstOpen(
|
||||
new_options.fast_sst_open);
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (!cfd->IsDropped()) {
|
||||
cfd->table_cache()->UpdateShouldPinTableHandles();
|
||||
cfd->table_cache()->SetFastSstOpen(new_options.fast_sst_open);
|
||||
}
|
||||
}
|
||||
wal_other_option_changed = mutable_db_options_.wal_bytes_per_sync !=
|
||||
@@ -6490,6 +6493,19 @@ Status DBImpl::IngestExternalFiles(
|
||||
TEST_SYNC_POINT("DBImpl::IngestExternalFiles:BeforeJobsRun:0");
|
||||
TEST_SYNC_POINT("DBImpl::IngestExternalFiles:BeforeJobsRun:1");
|
||||
TEST_SYNC_POINT("DBImpl::AddFile:Start");
|
||||
|
||||
// Acquire per-CF ingest_sst_lock as ReadLocks (shared) BEFORE the DB
|
||||
// mutex so the lock-acquisition order is ingest_sst_lock -> DB mutex
|
||||
// throughout. Use readlock so we still allow concurrent ingestions.
|
||||
std::vector<std::unique_ptr<ReadLock>> ingest_read_locks;
|
||||
ingest_read_locks.reserve(num_cfs);
|
||||
for (size_t i = 0; i != num_cfs; ++i) {
|
||||
auto* cfd = ingestion_jobs[i].GetColumnFamilyData();
|
||||
if (!cfd->IsDropped()) {
|
||||
ingest_read_locks.emplace_back(
|
||||
std::make_unique<ReadLock>(&cfd->GetIngestSstLock()));
|
||||
}
|
||||
}
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
TEST_SYNC_POINT("DBImpl::AddFile:MutexLock");
|
||||
@@ -6579,6 +6595,23 @@ Status DBImpl::IngestExternalFiles(
|
||||
ingestion_jobs[i].RegisterRange();
|
||||
}
|
||||
}
|
||||
// Now that Run() has assigned the actual seqno for each ingested file,
|
||||
// bump each affected memtable's ingest_seqno_barrier_ to that exact
|
||||
// value. We still hold the per-CF ingest_sst_lock as a ReadLock; any
|
||||
// concurrent conversion's TryWriteLock on the same lock fails, so no
|
||||
// converter can be reading or about to read the barrier while we
|
||||
// update it. After we release the ReadLocks at function exit, the
|
||||
// next conversion observes the new barrier and refuses any insert
|
||||
// with insert_seq < assigned.
|
||||
if (status.ok()) {
|
||||
for (size_t i = 0; i != num_cfs; ++i) {
|
||||
auto* cfd = ingestion_jobs[i].GetColumnFamilyData();
|
||||
SequenceNumber assigned = ingestion_jobs[i].MaxAssignedSequenceNumber();
|
||||
if (assigned > 0) {
|
||||
cfd->mem()->BumpIngestSeqnoBarrier(assigned);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (status.ok()) {
|
||||
ReadOptions read_options;
|
||||
read_options.fill_cache = args[0].options.fill_cache;
|
||||
|
||||
+4
-2
@@ -1788,7 +1788,7 @@ void DBIter::MaybeInsertRangeTombstone(const Slice& end_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert at the read sequence so the synthesized tombstone is visible only
|
||||
// Insert at the read sequence so the converted tombstone is visible only
|
||||
// to readers that could already observe the deletion run.
|
||||
SequenceNumber insert_seq = sequence_;
|
||||
|
||||
@@ -1819,8 +1819,10 @@ void DBIter::MaybeInsertRangeTombstone(const Slice& end_key) {
|
||||
}
|
||||
}
|
||||
|
||||
assert(cfh_ != nullptr);
|
||||
if (active_mem_->AddLogicallyRedundantRangeTombstone(
|
||||
insert_seq, range_tomb_first_key_.GetUserKey(), end_key)) {
|
||||
insert_seq, range_tomb_first_key_.GetUserKey(), end_key,
|
||||
cfh_->cfd()->GetIngestSstLock())) {
|
||||
RecordTick(statistics_, READ_PATH_RANGE_TOMBSTONES_INSERTED);
|
||||
ROCKS_LOG_DEBUG(logger_,
|
||||
"Inserted range tombstone [%s, %s) @ seq %" PRIu64
|
||||
|
||||
+132
-69
@@ -5583,13 +5583,13 @@ class ReadPathRangeTombstoneTest : public DBIteratorBaseTest,
|
||||
bool Forward() const { return GetParam(); }
|
||||
|
||||
void SetUp() override {
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"MemTable::AddLogicallyRedundantRangeTombstone:AddRange",
|
||||
[this](void* arg) {
|
||||
auto* range = static_cast<std::pair<Slice, Slice>*>(arg);
|
||||
inserted_ranges_.emplace_back(range->first.ToString(),
|
||||
range->second.ToString());
|
||||
attempted_insert_ranges_.emplace_back(range->first.ToString(),
|
||||
range->second.ToString());
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
}
|
||||
@@ -5633,9 +5633,9 @@ class ReadPathRangeTombstoneTest : public DBIteratorBaseTest,
|
||||
|
||||
void AssertRange(size_t idx, const std::string& start,
|
||||
const std::string& end) {
|
||||
ASSERT_LT(idx, inserted_ranges_.size());
|
||||
ASSERT_EQ(inserted_ranges_[idx].first, start);
|
||||
ASSERT_EQ(inserted_ranges_[idx].second, end);
|
||||
ASSERT_LT(idx, attempted_insert_ranges_.size());
|
||||
ASSERT_EQ(attempted_insert_ranges_[idx].first, start);
|
||||
ASSERT_EQ(attempted_insert_ranges_[idx].second, end);
|
||||
}
|
||||
|
||||
Slice MaxTimestamp(std::string* storage) const {
|
||||
@@ -5675,7 +5675,7 @@ class ReadPathRangeTombstoneTest : public DBIteratorBaseTest,
|
||||
<< iter->status().ToString();
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> inserted_ranges_;
|
||||
std::vector<std::pair<std::string, std::string>> attempted_insert_ranges_;
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(ReadPathRangeTombstoneTest, ReadPathRangeTombstoneTest,
|
||||
@@ -5698,19 +5698,28 @@ TEST_P(ReadPathRangeTombstoneTest, BasicInsertion) {
|
||||
|
||||
if (flush_before_read) {
|
||||
ASSERT_OK(Flush());
|
||||
// Memtable is empty after flush. AddLogicallyRedundantRangeTombstone
|
||||
// skips empty memtables.
|
||||
inserted_ranges_.clear();
|
||||
// After dropping the IsEmpty() fast-path in conversion, the now-empty
|
||||
// active memtable is a valid conversion target; the per-CF
|
||||
// ingest_sst_lock (held shared by ingestion) is the mechanism that
|
||||
// gates conversion vs ingestion, not memtable emptiness.
|
||||
attempted_insert_ranges_.clear();
|
||||
VerifyIteration({"a", "g", "h", "n"});
|
||||
ASSERT_EQ(inserted_ranges_.size(), 0);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
|
||||
if (forward) {
|
||||
AssertRange(0, "b", "g");
|
||||
AssertRange(1, "i", "n");
|
||||
} else {
|
||||
AssertRange(0, "i", "n");
|
||||
AssertRange(1, "b", "g");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
|
||||
VerifyIteration({"a", "g", "h", "n"});
|
||||
|
||||
ASSERT_EQ(inserted_ranges_.size(), 2);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
|
||||
if (forward) {
|
||||
AssertRange(0, "b", "g");
|
||||
AssertRange(1, "i", "n");
|
||||
@@ -5722,9 +5731,9 @@ TEST_P(ReadPathRangeTombstoneTest, BasicInsertion) {
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
2);
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
VerifyIteration({"a", "g", "h", "n"});
|
||||
ASSERT_EQ(inserted_ranges_.size(), 0);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 0);
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
2);
|
||||
@@ -5761,13 +5770,13 @@ TEST_P(ReadPathRangeTombstoneTest, MemtableSwitch) {
|
||||
/*flushed_point_dels=*/{},
|
||||
/*memtable_point_dels=*/{"b", "c", "d", "e", "f"});
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"MemTable::AddLogicallyRedundantRangeTombstone:AddRange",
|
||||
[this](void* arg) {
|
||||
auto* range = static_cast<std::pair<Slice, Slice>*>(arg);
|
||||
inserted_ranges_.emplace_back(range->first.ToString(),
|
||||
range->second.ToString());
|
||||
attempted_insert_ranges_.emplace_back(range->first.ToString(),
|
||||
range->second.ToString());
|
||||
auto* cfh =
|
||||
static_cast<ColumnFamilyHandleImpl*>(db_->DefaultColumnFamily());
|
||||
cfh->cfd()->mem()->MarkImmutable();
|
||||
@@ -5776,7 +5785,7 @@ TEST_P(ReadPathRangeTombstoneTest, MemtableSwitch) {
|
||||
|
||||
VerifyIteration({"a", "g", "h"});
|
||||
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "b", "g");
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_DISCARDED),
|
||||
@@ -5809,7 +5818,7 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorWithBounds) {
|
||||
VerifyIteration({"e", "f"}, ro);
|
||||
|
||||
// Both directions encounter two tombstone runs (a-d and g-j).
|
||||
ASSERT_EQ(inserted_ranges_.size(), 2);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
|
||||
if (Forward()) {
|
||||
// Forward: sees a-d tombstones first → [a, e), then g-j → [g, z).
|
||||
AssertRange(0, "a", "e");
|
||||
@@ -5830,9 +5839,9 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorWithBounds) {
|
||||
ASSERT_EQ(Get("j"), "NOT_FOUND");
|
||||
|
||||
// Second read: range tombstones already in memtable, no new insertion.
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
VerifyIteration({"e", "f"}, ro);
|
||||
ASSERT_EQ(inserted_ranges_.size(), 0);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 0);
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
2);
|
||||
@@ -5853,7 +5862,7 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorNoBounds) {
|
||||
|
||||
// Without bounds, only the a-d run (which has a live key boundary) gets
|
||||
// inserted. The g-j run at the end has no upper bound → dropped.
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "a", "e");
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
@@ -5871,7 +5880,7 @@ TEST_P(ReadPathRangeTombstoneTest, DirectionChange) {
|
||||
/*flushed_point_dels=*/{"c", "d", "e"},
|
||||
/*memtable_point_dels=*/{"f", "g", "j", "k", "l", "m", "n"});
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
|
||||
{
|
||||
ReadOptions ro;
|
||||
@@ -5918,7 +5927,7 @@ TEST_P(ReadPathRangeTombstoneTest, DirectionChange) {
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
ASSERT_EQ(inserted_ranges_.size(), 2);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
|
||||
if (forward_first) {
|
||||
AssertRange(0, "c", "h");
|
||||
AssertRange(1, "j", "o");
|
||||
@@ -5948,11 +5957,11 @@ TEST_P(ReadPathRangeTombstoneTest, MixedDeleteAndSingleDelete) {
|
||||
ASSERT_OK(SingleDelete("c"));
|
||||
ASSERT_OK(SingleDelete("e"));
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
|
||||
VerifyIteration({"a", "g", "h"});
|
||||
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "b", "g");
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
@@ -5982,11 +5991,11 @@ TEST_P(ReadPathRangeTombstoneTest, SingleDeleteOnlyRun) {
|
||||
ASSERT_OK(SingleDelete("e"));
|
||||
ASSERT_OK(SingleDelete("f"));
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
|
||||
VerifyIteration({"a", "g", "h"});
|
||||
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "b", "g");
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
@@ -5997,7 +6006,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterDefaultReadOptions) {
|
||||
// total_order_seek=false (default) with prefix extractor. Even when the
|
||||
// visible scan contains a valid in-prefix tombstone run [ba, bc), read-path
|
||||
// range conversion is disabled in this legacy prefix mode, so no
|
||||
// synthesized memtable tombstone is inserted.
|
||||
// converted memtable tombstone is inserted.
|
||||
Options options = CurrentOptions();
|
||||
options.min_tombstones_for_range_conversion = 2;
|
||||
options.statistics = CreateDBStatistics();
|
||||
@@ -6017,7 +6026,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterDefaultReadOptions) {
|
||||
ASSERT_OK(Delete("ba"));
|
||||
ASSERT_OK(Delete("bb"));
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
auto it = std::unique_ptr<Iterator>(db_->NewIterator(ReadOptions()));
|
||||
if (Forward()) {
|
||||
it->Seek("b");
|
||||
@@ -6032,7 +6041,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterDefaultReadOptions) {
|
||||
}
|
||||
}
|
||||
ASSERT_OK(it->status());
|
||||
ASSERT_EQ(inserted_ranges_.size(), 0u);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 0u);
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
0u);
|
||||
@@ -6088,7 +6097,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
|
||||
ASSERT_OK(del("ca"));
|
||||
ASSERT_OK(put("fa", "above"));
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
ReadOptions ro;
|
||||
ro.total_order_seek = true;
|
||||
if (use_udt) {
|
||||
@@ -6109,7 +6118,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
|
||||
}
|
||||
ASSERT_OK(it->status());
|
||||
// Tombstone crosses from prefix 'b' into 'c', terminated by live "cb".
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1u);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1u);
|
||||
if (use_udt) {
|
||||
AssertRange(0, std::string("ba") + ts, std::string("cb") + ts);
|
||||
} else {
|
||||
@@ -6121,7 +6130,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
|
||||
TEST_P(ReadPathRangeTombstoneTest, PrefixFilterPrefixSameAsStart) {
|
||||
// prefix_same_as_start=true: prefix filtering active, DBIter bounds the
|
||||
// scan to the seek prefix. An out-of-prefix tombstone ends the visible run,
|
||||
// but the synthesized range still stays within prefix by flushing to the
|
||||
// but the converted range still stays within prefix by flushing to the
|
||||
// last tracked in-prefix tombstone.
|
||||
// total_order_seek should not matter as we are guaranteed a total order view
|
||||
// within the prefix bounds.
|
||||
@@ -6169,7 +6178,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterPrefixSameAsStart) {
|
||||
ASSERT_OK(del("ca"));
|
||||
ASSERT_OK(put("fa", "above"));
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
ReadOptions ro;
|
||||
ro.prefix_same_as_start = true;
|
||||
ro.total_order_seek = total_order;
|
||||
@@ -6187,7 +6196,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterPrefixSameAsStart) {
|
||||
ASSERT_FALSE(it->Valid());
|
||||
}
|
||||
ASSERT_OK(it->status());
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1u);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1u);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(
|
||||
READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
1u);
|
||||
@@ -6235,7 +6244,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterOutOfDomainSeek) {
|
||||
ASSERT_OK(Put("bbdd", "v2"));
|
||||
ASSERT_OK(Put("cccc", "v3"));
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
ReadOptions ro;
|
||||
ro.prefix_same_as_start = true;
|
||||
auto it = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
@@ -6254,7 +6263,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterOutOfDomainSeek) {
|
||||
}
|
||||
}
|
||||
ASSERT_OK(it->status());
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1u);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1u);
|
||||
AssertRange(0, "bbbb", "bbdd");
|
||||
}
|
||||
|
||||
@@ -6279,10 +6288,10 @@ TEST_P(ReadPathRangeTombstoneTest, TableFilterNotAllowed) {
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Keep the active memtable non-empty so the read path has somewhere to store
|
||||
// the synthesized memtable range tombstone.
|
||||
// the converted memtable range tombstone.
|
||||
ASSERT_OK(Put("zz", "tail_mem"));
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
{
|
||||
// First iterator sees the full SST set, so converting [a, c) into a
|
||||
// memtable range tombstone is safe.
|
||||
@@ -6295,7 +6304,7 @@ TEST_P(ReadPathRangeTombstoneTest, TableFilterNotAllowed) {
|
||||
ASSERT_OK(it->status());
|
||||
}
|
||||
|
||||
ASSERT_GE(inserted_ranges_.size(), 1u);
|
||||
ASSERT_GE(attempted_insert_ranges_.size(), 1u);
|
||||
AssertRange(0, "a", "c");
|
||||
|
||||
{
|
||||
@@ -6304,7 +6313,7 @@ TEST_P(ReadPathRangeTombstoneTest, TableFilterNotAllowed) {
|
||||
return props.num_entries != 2;
|
||||
};
|
||||
// Hiding the two-delete SST would otherwise leave this iterator with a
|
||||
// partial SST view plus the previously synthesized memtable tombstone,
|
||||
// partial SST view plus the previously converted memtable tombstone,
|
||||
// allowing hidden SST state to affect the filtered read result.
|
||||
AssertTableFilterRangeConversionRejected(filtered_ro);
|
||||
}
|
||||
@@ -6329,7 +6338,7 @@ TEST_P(ReadPathRangeTombstoneTest, SnapshotPredatesMemtable) {
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("y", "vy"));
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.snapshot = snap;
|
||||
@@ -6402,7 +6411,7 @@ TEST_P(ReadPathRangeTombstoneTest, NoInsertionOnBlockCacheTierIncomplete) {
|
||||
|
||||
// No range tombstone should have been inserted despite meeting threshold,
|
||||
// because the iterator terminated with Incomplete (cache miss).
|
||||
ASSERT_EQ(inserted_ranges_.size(), 0);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 0);
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
0);
|
||||
@@ -6433,7 +6442,7 @@ TEST_P(ReadPathRangeTombstoneTest, SkipInsertionWhenCoveredByExistingRange) {
|
||||
ASSERT_OK(Delete(std::string(1, c)));
|
||||
}
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
ReadOptions ro;
|
||||
Slice upper("z");
|
||||
ro.iterate_upper_bound = &upper;
|
||||
@@ -6493,7 +6502,7 @@ TEST_P(ReadPathRangeTombstoneTest, UDTBasicScan) {
|
||||
ASSERT_EQ(keys, (std::vector<std::string>{"a", "g", "h"}));
|
||||
|
||||
// Range covers [b+ts, g+ts).
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, std::string("b") + ts, std::string("g") + ts);
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
@@ -6502,7 +6511,7 @@ TEST_P(ReadPathRangeTombstoneTest, UDTBasicScan) {
|
||||
|
||||
// Regression test: an older UDT read timestamp can hide newer live versions
|
||||
// inside a delete run. Range conversion must stay disabled in that case, or
|
||||
// the synthesized range tombstone will incorrectly hide those newer versions
|
||||
// the converted range tombstone will incorrectly hide those newer versions
|
||||
// for later max-timestamp reads.
|
||||
TEST_P(ReadPathRangeTombstoneTest, UDTOlderTimestampDisablesInsertion) {
|
||||
Options options = CurrentOptions();
|
||||
@@ -6535,12 +6544,12 @@ TEST_P(ReadPathRangeTombstoneTest, UDTOlderTimestampDisablesInsertion) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "b", ts3_slice, "vb3"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "c", ts3_slice, "vc3"));
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
ReadOptions old_ro;
|
||||
old_ro.timestamp = &ts2_slice;
|
||||
VerifyIteration({"d"}, old_ro);
|
||||
|
||||
ASSERT_EQ(inserted_ranges_.size(), 0u);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 0u);
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
0);
|
||||
@@ -6601,7 +6610,7 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(keys.size(), 4);
|
||||
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (Forward()) {
|
||||
// Forward exhaustion: range is [e+ts, z+min_ts).
|
||||
AssertRange(0, std::string("e") + ts, std::string("z") + min_ts);
|
||||
@@ -6641,7 +6650,7 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
}
|
||||
SetupTestData('a', 'h', /*flushed_point_dels=*/{},
|
||||
/*memtable_point_dels=*/{"e", "f", "g", "h"}, ts_ptr);
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
|
||||
ReadOptions ro;
|
||||
if (use_udt) {
|
||||
@@ -6666,9 +6675,9 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
|
||||
if (Forward()) {
|
||||
// No upper bound → trailing tombstone run has no end key → dropped.
|
||||
ASSERT_EQ(inserted_ranges_.size(), 0);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 0);
|
||||
} else {
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (use_udt) {
|
||||
std::string min_ts(sizeof(uint64_t), '\0');
|
||||
AssertRange(0, std::string("e") + ts, std::string("h") + min_ts);
|
||||
@@ -6710,7 +6719,7 @@ TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
|
||||
}
|
||||
SetupTestData('a', 'h', /*flushed_point_dels=*/{},
|
||||
/*memtable_point_dels=*/{"e", "f", "g", "h"}, ts_ptr);
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
|
||||
ReadOptions ro;
|
||||
if (use_udt) {
|
||||
@@ -6735,7 +6744,7 @@ TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(keys, (std::vector<std::string>{"a", "b", "c", "d"}));
|
||||
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (use_udt) {
|
||||
// Forward end key: upper bound padded with min_ts.
|
||||
// Reverse end key: upper bound padded with max_ts (via
|
||||
@@ -6780,7 +6789,7 @@ TEST_P(ReadPathRangeTombstoneTest, LowerBoundTruncatesReverse) {
|
||||
'a', 'j', /*flushed_point_dels=*/{},
|
||||
/*memtable_point_dels=*/{"a", "b", "c", "d", "e", "f", "g", "h"},
|
||||
ts_ptr);
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
|
||||
ReadOptions ro;
|
||||
if (use_udt) {
|
||||
@@ -6809,7 +6818,7 @@ TEST_P(ReadPathRangeTombstoneTest, LowerBoundTruncatesReverse) {
|
||||
ASSERT_EQ(keys, (std::vector<std::string>{"i", "j"}));
|
||||
|
||||
// Both directions produce one range covering tombstones e-h.
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (use_udt) {
|
||||
AssertRange(0, std::string("e") + ts, std::string("i") + ts);
|
||||
} else {
|
||||
@@ -6860,11 +6869,11 @@ TEST_P(ReadPathRangeTombstoneTest,
|
||||
ASSERT_OK(Delete("c"));
|
||||
|
||||
// Iterate at the latest sequence. Both b and c hit the reseek path and
|
||||
// synthesize a range tombstone [b, d) for later readers at the same seq.
|
||||
inserted_ranges_.clear();
|
||||
// convert a range tombstone [b, d) for later readers at the same seq.
|
||||
attempted_insert_ranges_.clear();
|
||||
VerifyIteration({"a", "d"});
|
||||
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "b", "d");
|
||||
|
||||
// Read with the earlier snapshot. The point deletes (seq 11-12) are not
|
||||
@@ -6906,17 +6915,71 @@ TEST_P(ReadPathRangeTombstoneTest, InvisibleKeysDontBreakTombstoneRun) {
|
||||
|
||||
ReadOptions ro;
|
||||
ro.snapshot = snap;
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
VerifyIteration({"a", "f"}, ro);
|
||||
|
||||
// Invisible keys c and e should not break the tombstone run.
|
||||
// 2 tombstones (b, d) ≥ threshold → range [b, f).
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "b", "f");
|
||||
|
||||
db_->ReleaseSnapshot(snap);
|
||||
}
|
||||
|
||||
// Regression test: a converted tombstone can land in the current memtable at
|
||||
// an older snapshot sequence than a live point already ingested into an older
|
||||
// L0 file. Latest point lookups must continue searching that older file when
|
||||
// its sequence range can still contain a newer point version.
|
||||
TEST_P(ReadPathRangeTombstoneTest, NewerPointInOlderFileStillVisible) {
|
||||
Options options = CurrentOptions();
|
||||
options.min_tombstones_for_range_conversion = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
options.statistics = CreateDBStatistics();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("b", "vb"));
|
||||
ASSERT_OK(Put("c", "vc"));
|
||||
ASSERT_OK(Put("d", "vd"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Delete("b"));
|
||||
ASSERT_OK(Delete("c"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Keep the active memtable older than the snapshot so the read path is
|
||||
// allowed to convert a tombstone into it later.
|
||||
ASSERT_OK(Put("z", "vz_anchor"));
|
||||
const Snapshot* snap = db_->GetSnapshot();
|
||||
|
||||
const std::string ingest_file = dbname_ + "_live_c.sst";
|
||||
{
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.Open(ingest_file));
|
||||
ASSERT_OK(writer.Put("c", "vc_live"));
|
||||
ASSERT_OK(writer.Finish());
|
||||
}
|
||||
|
||||
IngestExternalFileOptions ifo;
|
||||
ifo.allow_blocking_flush = false;
|
||||
ASSERT_OK(db_->IngestExternalFile({ingest_file}, ifo));
|
||||
ASSERT_EQ(Get("c"), "vc_live");
|
||||
|
||||
attempted_insert_ranges_.clear();
|
||||
ReadOptions snap_ro;
|
||||
snap_ro.snapshot = snap;
|
||||
VerifyIteration({"d", "z"}, snap_ro);
|
||||
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "b", "d");
|
||||
|
||||
const Snapshot* latest = db_->GetSnapshot();
|
||||
ASSERT_EQ(Get("c", latest), "vc_live");
|
||||
ASSERT_EQ(MultiGet({"c"}, latest), (std::vector<std::string>{"vc_live"}));
|
||||
|
||||
db_->ReleaseSnapshot(latest);
|
||||
db_->ReleaseSnapshot(snap);
|
||||
}
|
||||
|
||||
// Regression test: SeekToLast() after Seek() must clear stale saved_key_ to
|
||||
// avoid corrupting range tombstone tracking bounds.
|
||||
TEST_P(ReadPathRangeTombstoneTest, SeekToLastStaleSavedKey) {
|
||||
@@ -6975,7 +7038,7 @@ TEST_P(ReadPathRangeTombstoneTest, SeekToLastTombstones) {
|
||||
ASSERT_OK(Delete("x"));
|
||||
ASSERT_OK(Delete("y"));
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ReadOptions()));
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
|
||||
iter->Seek("a");
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
@@ -6985,20 +7048,20 @@ TEST_P(ReadPathRangeTombstoneTest, SeekToLastTombstones) {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ("z", iter->key().ToString());
|
||||
ASSERT_EQ(inserted_ranges_.size(), 0u);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 0u);
|
||||
|
||||
// Reverse iteration skips deleted x and y.
|
||||
iter->Prev();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ("w", iter->key().ToString());
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1u);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1u);
|
||||
AssertRange(0, "x", "z");
|
||||
}
|
||||
|
||||
// Regression test for a crash-test pattern where the interior between two
|
||||
// point tombstones is hidden by a later DeleteRange. A latest iterator may
|
||||
// still synthesize a redundant range tombstone, but an older snapshot must
|
||||
// still convert a redundant range tombstone, but an older snapshot must
|
||||
// continue to see the pre-DeleteRange live key after iteration.
|
||||
TEST_P(ReadPathRangeTombstoneTest,
|
||||
RangeDeletedInteriorPreservesOlderSnapshots) {
|
||||
@@ -7028,11 +7091,11 @@ TEST_P(ReadPathRangeTombstoneTest,
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "c", "m"));
|
||||
|
||||
inserted_ranges_.clear();
|
||||
attempted_insert_ranges_.clear();
|
||||
VerifyIteration({"a", "n"});
|
||||
|
||||
// Materialize the redundant tombstone at the latest read sequence only.
|
||||
ASSERT_EQ(inserted_ranges_.size(), 1);
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "b", "n");
|
||||
|
||||
snap_value.clear();
|
||||
|
||||
@@ -8149,6 +8149,47 @@ TEST_F(DBTest2, FastSstOpenIngestion) {
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenDisableAfterMetadataPersisted) {
|
||||
// Verify that disabling fast_sst_open prevents previously-persisted
|
||||
// metadata from being passed to NewRandomAccessFile. This is critical
|
||||
// for cases where stale metadata (e.g. expired credentials) would
|
||||
// cause file open failures.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = true;
|
||||
options.create_if_missing = true;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_disable_after");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
// Open with fast_sst_open enabled, write and flush to persist metadata
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
db.reset();
|
||||
|
||||
// Reopen with fast_sst_open DISABLED — metadata is in the MANIFEST
|
||||
// but should NOT be passed to the filesystem
|
||||
options.fast_sst_open = false;
|
||||
test_fs->ResetCounters();
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
|
||||
// No metadata should have been passed despite being in the MANIFEST
|
||||
ASSERT_EQ(0, test_fs->GetMetadataPassedOnOpenCount());
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+38
-6
@@ -918,12 +918,11 @@ void MemTable::ConstructFragmentedRangeTombstones() {
|
||||
}
|
||||
}
|
||||
|
||||
bool MemTable::AddLogicallyRedundantRangeTombstone(SequenceNumber seq,
|
||||
const Slice& start_key,
|
||||
const Slice& end_key) {
|
||||
// Fast path: skip if already immutable or empty. Some code paths (i.e.
|
||||
// ExternalFileIngestion) rely ensuring memtable is empty after flushing.
|
||||
if (is_immutable_.LoadRelaxed() || IsEmpty()) {
|
||||
bool MemTable::AddLogicallyRedundantRangeTombstone(
|
||||
SequenceNumber seq, const Slice& start_key, const Slice& end_key,
|
||||
port::RWMutex& ingest_sst_lock) {
|
||||
// Fast path: skip if already immutable.
|
||||
if (is_immutable_.LoadRelaxed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -939,6 +938,33 @@ bool MemTable::AddLogicallyRedundantRangeTombstone(SequenceNumber seq,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Range tombstone reads have an assumption that all levels below it have a
|
||||
// LOWER seqno than it, so it is safe to skip reading files. Normally, this is
|
||||
// true, but range tombstone conversion creates an exception.
|
||||
//
|
||||
// The inserted range tombstone uses iterator seqno. There are guards to
|
||||
// ensure that we only insert it if it is within current memtable's bounds,
|
||||
// BUT an external file ingestion can break that still, as the newly ingested
|
||||
// L0 file will be assigned a higher seqno than an earlier iterator.
|
||||
//
|
||||
// So the solution here is to use a RW lock + ingest seqno to gate range
|
||||
// tombstone conversions. An added side effect is also we can no longer insert
|
||||
// to memtables while a file ingestion is in progress, which is an expectation
|
||||
// of file ingestion. Note we expect this insertion to be rare, and we do not
|
||||
// want to limit concurrent external file ingestions so the conversion path
|
||||
// uses a write lock while the ingestion path uses a read lock.
|
||||
TryWriteLock ingest_wl(&ingest_sst_lock);
|
||||
if (!ingest_wl.OwnsLock()) {
|
||||
return false;
|
||||
}
|
||||
// After ingestion releases its WriteLock, an iterator with an older
|
||||
// snapshot could still try to convert a tombstone whose seq sits
|
||||
// below the just-ingested file's seq. The barrier persists past the end
|
||||
// of the ingestion that bumped it and refuses such inserts.
|
||||
if (seq < ingest_seqno_barrier_.LoadRelaxed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MemTablePostProcessInfo post_process_info;
|
||||
Status s = Add(seq, kTypeRangeDeletion, start_key, end_key,
|
||||
nullptr /* kv_prot_info */, true /* allow_concurrent */,
|
||||
@@ -950,6 +976,12 @@ bool MemTable::AddLogicallyRedundantRangeTombstone(SequenceNumber seq,
|
||||
return false;
|
||||
}
|
||||
|
||||
void MemTable::BumpIngestSeqnoBarrier(SequenceNumber y) {
|
||||
if (ingest_seqno_barrier_.LoadRelaxed() < y) {
|
||||
ingest_seqno_barrier_.StoreRelaxed(y);
|
||||
}
|
||||
}
|
||||
|
||||
port::RWMutex* MemTable::GetLock(const Slice& key) {
|
||||
return &locks_[GetSliceRangedNPHash(key, locks_.size())];
|
||||
}
|
||||
|
||||
+23
-7
@@ -328,11 +328,13 @@ class ReadOnlyMemTable {
|
||||
// Adding a range tombstone may fail if
|
||||
// - memtable switches to immutable state
|
||||
// - a range tombstone with the same key+seq already exists (duplicate insert)
|
||||
//
|
||||
// - the per-memtable ingest seqno barrier already exceeds `seq` (an
|
||||
// ingestion has committed an L0 file at a seq that this converted
|
||||
// tombstone would shadow) or an ingestion is in progress.
|
||||
// Returns true if the range tombstone was inserted, false if skipped.
|
||||
virtual bool AddLogicallyRedundantRangeTombstone(SequenceNumber /*seq*/,
|
||||
const Slice& /*start_key*/,
|
||||
const Slice& /*end_key*/) {
|
||||
virtual bool AddLogicallyRedundantRangeTombstone(
|
||||
SequenceNumber /*seq*/, const Slice& /*start_key*/,
|
||||
const Slice& /*end_key*/, port::RWMutex& /*ingest_sst_lock*/) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -858,9 +860,19 @@ class MemTable final : public ReadOnlyMemTable {
|
||||
// SwitchMemtable() may fail.
|
||||
void ConstructFragmentedRangeTombstones();
|
||||
|
||||
bool AddLogicallyRedundantRangeTombstone(SequenceNumber seq,
|
||||
const Slice& start_key,
|
||||
const Slice& end_key) override;
|
||||
bool AddLogicallyRedundantRangeTombstone(
|
||||
SequenceNumber seq, const Slice& start_key, const Slice& end_key,
|
||||
port::RWMutex& ingest_sst_lock) override;
|
||||
|
||||
// Monotonically raises ingest_seqno_barrier_ to `y` (no-op if `y` is not
|
||||
// greater than the current value). The conversion's barrier check
|
||||
// (`seq < ingest_seqno_barrier_.LoadRelaxed()`) refuses converted
|
||||
// range tombstones that would shadow a just-installed L0 file.
|
||||
//
|
||||
// REQUIRES: DB mutex held by the caller. The DB mutex serializes all
|
||||
// callers, so the load-then-store pattern is race-free without needing
|
||||
// a CAS loop. Only IngestExternalFiles calls this.
|
||||
void BumpIngestSeqnoBarrier(SequenceNumber y);
|
||||
|
||||
bool IsFragmentedRangeTombstonesConstructed() const override {
|
||||
return fragmented_range_tombstone_list_.get() != nullptr ||
|
||||
@@ -922,6 +934,10 @@ class MemTable final : public ReadOnlyMemTable {
|
||||
// if not set.
|
||||
std::atomic<SequenceNumber> earliest_seqno_;
|
||||
|
||||
// Seqno of the latest ingested external SST. See also
|
||||
// ColumnFamilyData::ingest_sst_lock_.
|
||||
RelaxedAtomic<SequenceNumber> ingest_seqno_barrier_{0};
|
||||
|
||||
SequenceNumber creation_seq_;
|
||||
|
||||
// the earliest log containing a prepared section
|
||||
|
||||
+6
-2
@@ -69,13 +69,14 @@ TableCache::TableCache(const ImmutableOptions& ioptions,
|
||||
const FileOptions* file_options, Cache* const cache,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_session_id)
|
||||
const std::string& db_session_id, bool fast_sst_open)
|
||||
: ioptions_(ioptions),
|
||||
file_options_(*file_options),
|
||||
cache_(cache),
|
||||
immortal_tables_(false),
|
||||
should_pin_table_handles_(cache_.get()->GetCapacity() >=
|
||||
kInfiniteCapacity),
|
||||
fast_sst_open_(fast_sst_open),
|
||||
block_cache_tracer_(block_cache_tracer),
|
||||
loader_mutex_(kLoadConcurency),
|
||||
io_tracer_(io_tracer),
|
||||
@@ -107,8 +108,11 @@ Status TableCache::GetTableReader(
|
||||
fopts.file_checksum_func_name = file_meta.file_checksum_func_name;
|
||||
// Pass file open metadata for fast SST open. Use a local copy since
|
||||
// fopts.file_metadata is a non-owning pointer and file_meta is const.
|
||||
// Only pass metadata when fast_sst_open is enabled; otherwise ignore
|
||||
// previously persisted metadata (e.g. stale filesystem credentials).
|
||||
std::string file_open_metadata_copy;
|
||||
if (!file_meta.file_open_metadata.empty()) {
|
||||
if (fast_sst_open_.load(std::memory_order_relaxed) &&
|
||||
!file_meta.file_open_metadata.empty()) {
|
||||
file_open_metadata_copy = file_meta.file_open_metadata;
|
||||
fopts.file_metadata = &file_open_metadata_copy;
|
||||
RecordTick(ioptions_.stats, FILE_OPEN_METADATA_PASSED);
|
||||
|
||||
+6
-1
@@ -54,7 +54,7 @@ class TableCache {
|
||||
const FileOptions* storage_options, Cache* cache,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_session_id);
|
||||
const std::string& db_session_id, bool fast_sst_open = false);
|
||||
~TableCache();
|
||||
|
||||
// Cache interface for table cache
|
||||
@@ -270,6 +270,10 @@ class TableCache {
|
||||
cache_.get()->GetCapacity() >= kInfiniteCapacity;
|
||||
}
|
||||
|
||||
void SetFastSstOpen(bool enabled) {
|
||||
fast_sst_open_.store(enabled, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
private:
|
||||
// Build a table reader
|
||||
Status GetTableReader(const ReadOptions& ro, const FileOptions& file_options,
|
||||
@@ -312,6 +316,7 @@ class TableCache {
|
||||
std::string row_cache_id_;
|
||||
bool immortal_tables_;
|
||||
bool should_pin_table_handles_;
|
||||
std::atomic<bool> fast_sst_open_;
|
||||
BlockCacheTracer* const block_cache_tracer_;
|
||||
Striped<CacheAlignedWrapper<port::Mutex>> loader_mutex_;
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
|
||||
+3
-2
@@ -5599,7 +5599,7 @@ VersionSet::VersionSet(
|
||||
: column_family_set_(new ColumnFamilySet(
|
||||
dbname, _db_options, storage_options, table_cache,
|
||||
write_buffer_manager, write_controller, block_cache_tracer, io_tracer,
|
||||
db_id, db_session_id)),
|
||||
db_id, db_session_id, mutable_db_options.fast_sst_open)),
|
||||
table_cache_(table_cache),
|
||||
env_(_db_options->env),
|
||||
fs_(_db_options->fs, io_tracer),
|
||||
@@ -5808,7 +5808,8 @@ void VersionSet::Reset() {
|
||||
// options.write_dbid_to_manifest is false (default).
|
||||
column_family_set_.reset(new ColumnFamilySet(
|
||||
dbname_, db_options_, file_options_, table_cache_, wbm, wc,
|
||||
block_cache_tracer_, io_tracer_, db_id_, db_session_id_));
|
||||
block_cache_tracer_, io_tracer_, db_id_, db_session_id_,
|
||||
column_family_set_->GetFastSstOpen()));
|
||||
}
|
||||
db_id_.clear();
|
||||
next_file_number_.store(2);
|
||||
|
||||
@@ -1419,8 +1419,18 @@ struct AdvancedColumnFamilyOptions {
|
||||
// (ReadOptions::total_order_seek / ReadOptions::auto_prefix_mode) nor
|
||||
// bounded by ReadOptions::prefix_same_as_start
|
||||
//
|
||||
// It also requires an active mutable memtable, and insertion is skipped when
|
||||
// that memtable is empty.
|
||||
// Even if the above restrictions are met, there are still scenarios where a
|
||||
// converted range tombstone may be discarded:
|
||||
// * The snapshot's active mutable memtable has already become immutable.
|
||||
// * The iterator's snapshot seq is below the active memtable's earliest
|
||||
// sequence number.
|
||||
// * A range tombstone covering [first_tombstone_key, next_live_key) is
|
||||
// already present in the memtable.
|
||||
// * A WritePrepared/WriteUnprepared transaction read callback is in use
|
||||
// and the snapshot seq is at or above its min uncommitted seq.
|
||||
// * An IngestExternalFile call is currently in flight on this column
|
||||
// family OR the inserted range tombstone seqno would be lower than the
|
||||
// ingested file seqno.
|
||||
//
|
||||
// Read-write iterators using ReadOptions::table_filter are rejected while
|
||||
// this option is enabled, see more details in ReadOptions::table_filter
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 11
|
||||
#define ROCKSDB_MINOR 2
|
||||
#define ROCKSDB_PATCH 0
|
||||
#define ROCKSDB_PATCH 2
|
||||
|
||||
// Make it easy to do conditional compilation based on version checks, i.e.
|
||||
// #if ROCKSDB_VERSION_GE(4, 5, 6)
|
||||
|
||||
@@ -161,10 +161,34 @@ void RWMutex::ReadLock() {
|
||||
PthreadCall("read lock", pthread_rwlock_rdlock(&mu_));
|
||||
}
|
||||
|
||||
bool RWMutex::TryReadLock() {
|
||||
int result = pthread_rwlock_tryrdlock(&mu_);
|
||||
if (result == 0) {
|
||||
return true;
|
||||
}
|
||||
if (result == EBUSY) {
|
||||
return false;
|
||||
}
|
||||
PthreadCall("try read lock", result);
|
||||
return false;
|
||||
}
|
||||
|
||||
void RWMutex::WriteLock() {
|
||||
PthreadCall("write lock", pthread_rwlock_wrlock(&mu_));
|
||||
}
|
||||
|
||||
bool RWMutex::TryWriteLock() {
|
||||
int result = pthread_rwlock_trywrlock(&mu_);
|
||||
if (result == 0) {
|
||||
return true;
|
||||
}
|
||||
if (result == EBUSY) {
|
||||
return false;
|
||||
}
|
||||
PthreadCall("try write lock", result);
|
||||
return false;
|
||||
}
|
||||
|
||||
void RWMutex::ReadUnlock() {
|
||||
PthreadCall("read unlock", pthread_rwlock_unlock(&mu_));
|
||||
}
|
||||
|
||||
@@ -136,7 +136,9 @@ class RWMutex {
|
||||
~RWMutex();
|
||||
|
||||
void ReadLock();
|
||||
bool TryReadLock();
|
||||
void WriteLock();
|
||||
bool TryWriteLock();
|
||||
void ReadUnlock();
|
||||
void WriteUnlock();
|
||||
void AssertHeld() const {}
|
||||
|
||||
@@ -152,8 +152,12 @@ class RWMutex {
|
||||
|
||||
void ReadLock() { AcquireSRWLockShared(&srwLock_); }
|
||||
|
||||
bool TryReadLock() { return TryAcquireSRWLockShared(&srwLock_) != 0; }
|
||||
|
||||
void WriteLock() { AcquireSRWLockExclusive(&srwLock_); }
|
||||
|
||||
bool TryWriteLock() { return TryAcquireSRWLockExclusive(&srwLock_) != 0; }
|
||||
|
||||
void ReadUnlock() { ReleaseSRWLockShared(&srwLock_); }
|
||||
|
||||
void WriteUnlock() { ReleaseSRWLockExclusive(&srwLock_); }
|
||||
|
||||
+8
-17
@@ -482,8 +482,7 @@ default_params = {
|
||||
"auto_refresh_iterator_with_snapshot": lambda: random.choice([0, 1]),
|
||||
"memtable_op_scan_flush_trigger": lambda: random.choice([0, 10, 100, 1000]),
|
||||
"memtable_avg_op_scan_flush_trigger": lambda: random.choice([0, 2, 20, 200]),
|
||||
# TODO(jkangs): Change back to [0, 2, 2, 4, 16] once range tombstone conversion stabilizes
|
||||
"min_tombstones_for_range_conversion": lambda: random.choice([0]),
|
||||
"min_tombstones_for_range_conversion": lambda: random.choice([0, 2, 2, 4, 16]),
|
||||
"ingest_wbwi_one_in": lambda: random.choice([0, 0, 100, 500]),
|
||||
"universal_reduce_file_locking": lambda: random.randint(0, 1),
|
||||
"compression_manager": lambda: random.choice(
|
||||
@@ -930,6 +929,8 @@ def finalize_and_sanitize(src_params):
|
||||
# range tombstone conversion is enabled, because conversion must see
|
||||
# the full relevant SST set before synthesizing a memtable tombstone.
|
||||
dest_params["use_sqfc_for_range_queries"] = 0
|
||||
# Delete range not compatible with inplace_update_support
|
||||
dest_params["inplace_update_support"] = 0
|
||||
if (
|
||||
dest_params["use_direct_io_for_flush_and_compaction"] == 1
|
||||
or dest_params["use_direct_reads"] == 1
|
||||
@@ -1710,9 +1711,7 @@ def collect_diagnostic_roots(base_paths, stdout, stderr):
|
||||
pruned_roots = []
|
||||
for root in sorted(roots, key=lambda path: (len(path), path)):
|
||||
if any(
|
||||
root == existing
|
||||
or existing == os.sep
|
||||
or root.startswith(existing + os.sep)
|
||||
root == existing or existing == os.sep or root.startswith(existing + os.sep)
|
||||
for existing in pruned_roots
|
||||
):
|
||||
continue
|
||||
@@ -1764,9 +1763,7 @@ def collect_directory_usage(root):
|
||||
with os.scandir(dirpath) as iterator:
|
||||
children = sorted(list(iterator), key=lambda entry: entry.name)
|
||||
except OSError as exc:
|
||||
errors.append(
|
||||
(dirpath, f"failed to enumerate directory contents: {exc}")
|
||||
)
|
||||
errors.append((dirpath, f"failed to enumerate directory contents: {exc}"))
|
||||
return summary
|
||||
|
||||
for child in children:
|
||||
@@ -1774,9 +1771,7 @@ def collect_directory_usage(root):
|
||||
if child.is_dir(follow_symlinks=False):
|
||||
summary["local_dir_count"] += 1
|
||||
child_summary = walk(child.path)
|
||||
summary["subtree_file_count"] += child_summary[
|
||||
"subtree_file_count"
|
||||
]
|
||||
summary["subtree_file_count"] += child_summary["subtree_file_count"]
|
||||
summary["subtree_bytes"] += child_summary["subtree_bytes"]
|
||||
continue
|
||||
|
||||
@@ -1799,9 +1794,7 @@ def collect_directory_usage(root):
|
||||
local_usage["count"] += 1
|
||||
local_usage["bytes"] += file_size
|
||||
|
||||
global_usage = global_suffixes.setdefault(
|
||||
suffix, {"count": 0, "bytes": 0}
|
||||
)
|
||||
global_usage = global_suffixes.setdefault(suffix, {"count": 0, "bytes": 0})
|
||||
global_usage["count"] += 1
|
||||
global_usage["bytes"] += file_size
|
||||
|
||||
@@ -1813,9 +1806,7 @@ def collect_directory_usage(root):
|
||||
|
||||
|
||||
def sorted_suffix_usage(suffixes):
|
||||
return sorted(
|
||||
suffixes.items(), key=lambda item: (-item[1]["bytes"], item[0])
|
||||
)
|
||||
return sorted(suffixes.items(), key=lambda item: (-item[1]["bytes"], item[0]))
|
||||
|
||||
|
||||
def format_directory_usage(root):
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Prefix filter changes - when seeking to a key that is out of domain, and total_order_seek is false, total_order_seek is treated as if it were true. When prefix_same_as_start = true, now iterating past a key that is out of domain invalidates the iterator. The existing behavior does not check for InDomain in the DBIter, so Transform() can produce undefined behavior (e.g. key of size 3 on FixedPrefixTransform(4)).
|
||||
@@ -1 +0,0 @@
|
||||
Wide-column entities with blob-backed columns now use a new V2 on-disk encoding; older RocksDB versions that do not support wide-column blob separation will reject DBs or SSTs containing those entities.
|
||||
@@ -1 +0,0 @@
|
||||
Fix blob garbage accounting for blob direct-write flushes so flush-time filtering and overwrite elision correctly register obsolete blob bytes in blob metadata.
|
||||
@@ -1 +0,0 @@
|
||||
Fix a memory accounting leak in IODispatcher where ReadIndex() moved block values out of ReadSet without releasing the associated prefetch memory, causing subsequent prefetches to be blocked when max_prefetch_memory_bytes was set.
|
||||
@@ -1 +0,0 @@
|
||||
Fix MultiScan to fall back to synchronous coalesced reads when async I/O is unsupported at runtime.
|
||||
@@ -1 +0,0 @@
|
||||
Added experimental `DBOptions::fast_sst_open` option. When enabled, RocksDB retrieves opaque file system metadata for SST files after flush, compaction, and external file ingestion, persists it in the MANIFEST, and passes it back to the file system on subsequent file opens to accelerate DB open time.
|
||||
@@ -1 +0,0 @@
|
||||
Added new option `min_tombstones_for_range_conversion` in `AdvancedColumnFamilyOptions`. When set to a non-zero value N, forward or reverse iteration will convert N or more contiguous point tombstones into a range tombstone in the mutable memtable. Future read operations will then be able to benefit from range tombstone optimizations. There are some limitations when it comes to table_filters, prefix_filters, and UDTs. See header comments for more details.
|
||||
@@ -1 +0,0 @@
|
||||
Added read-triggered compaction: a new column family option `read_triggered_compaction_threshold` (default 0, disabled) that marks SST files for compaction when their read frequency (`num_collapsible_entry_reads_sampled / file_size`) exceeds the threshold. This helps reduce read amplification for frequently-read ("hot") keys. A new DB option `max_compaction_trigger_wakeup_seconds` (default 43200s / 12 hours) controls the maximum interval for periodic compaction score re-evaluation, which is necessary for this feature to work on quiet (no-write) databases.
|
||||
@@ -1 +0,0 @@
|
||||
Added new `WideColumnBlobResolver` interface and `CompactionFilter::FilterV4()` method, including `ResolveColumn()` / `ResolveColumns()` helpers for lazy loading blob column values in wide-column entities during compaction. This allows compaction filters to resolve blob values on-demand, avoiding unnecessary I/O for blob columns they don't need to access.
|
||||
@@ -1 +0,0 @@
|
||||
Changed experimental feature `ExternalTableReader::Get` and `ExternalTableReader::MultiGet` to use `PinnableSlice` instead of `std::string` for output values, enabling zero-copy pinning. This will break existing implementations.
|
||||
@@ -1 +0,0 @@
|
||||
Added `SstFileReader::Get` and `SstFileReader::MultiGet` overloads that accept `PinnableSlice`/`std::vector<PinnableSlice>*`, enabling zero-copy reads when the underlying `TableReader` supports pinning.
|
||||
@@ -63,6 +63,30 @@ class ReadLock {
|
||||
port::RWMutex* const mu_;
|
||||
};
|
||||
|
||||
//
|
||||
// Try to acquire a ReadLock on the specified RWMutex without blocking.
|
||||
//
|
||||
class TryReadLock {
|
||||
public:
|
||||
explicit TryReadLock(port::RWMutex* mu)
|
||||
: mu_(mu), owns_(mu_->TryReadLock()) {}
|
||||
// No copying allowed
|
||||
TryReadLock(const TryReadLock&) = delete;
|
||||
void operator=(const TryReadLock&) = delete;
|
||||
|
||||
~TryReadLock() {
|
||||
if (owns_) {
|
||||
mu_->ReadUnlock();
|
||||
}
|
||||
}
|
||||
|
||||
bool OwnsLock() const { return owns_; }
|
||||
|
||||
private:
|
||||
port::RWMutex* const mu_;
|
||||
const bool owns_;
|
||||
};
|
||||
|
||||
//
|
||||
// Automatically unlock a locked mutex when the object is destroyed
|
||||
//
|
||||
@@ -97,6 +121,33 @@ class WriteLock {
|
||||
port::RWMutex* const mu_;
|
||||
};
|
||||
|
||||
//
|
||||
// Try to acquire a WriteLock on the specified RWMutex without blocking.
|
||||
// If acquired, the lock is released when the object goes out of scope.
|
||||
// If not acquired, the destructor is a no-op. Use OwnsLock() to check
|
||||
// whether the lock was acquired.
|
||||
//
|
||||
class TryWriteLock {
|
||||
public:
|
||||
explicit TryWriteLock(port::RWMutex* mu)
|
||||
: mu_(mu), owns_(mu_->TryWriteLock()) {}
|
||||
// No copying allowed
|
||||
TryWriteLock(const TryWriteLock&) = delete;
|
||||
void operator=(const TryWriteLock&) = delete;
|
||||
|
||||
~TryWriteLock() {
|
||||
if (owns_) {
|
||||
mu_->WriteUnlock();
|
||||
}
|
||||
}
|
||||
|
||||
bool OwnsLock() const { return owns_; }
|
||||
|
||||
private:
|
||||
port::RWMutex* const mu_;
|
||||
const bool owns_;
|
||||
};
|
||||
|
||||
//
|
||||
// SpinMutex has very low overhead for low-contention cases. Method names
|
||||
// are chosen so you can use std::unique_lock or std::lock_guard with it.
|
||||
|
||||
Reference in New Issue
Block a user