Compare commits

...

11 Commits

Author SHA1 Message Date
Xingbo Wang 6c9b3a8e99 Update HISTORY.md for 11.2.2 2026-05-07 04:22:47 -07:00
Xingbo Wang 1ea163bdcf Update version for 11.2.2 2026-05-07 04:21:55 -07:00
xingbowang ed5555ba44 Keep remote compaction stats serialization compatible with 11.1 (#14712)
Summary:
- Reapply c4941760c9 so remote compaction serializes InternalStats::CompactionStats::counts with the pre-11.2 compaction-reason count.
- Keep remote compaction result metadata compatible with 11.1 readers and writers until the stats serialization path has version-aware array handling.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14712

Test Plan:
- make format-auto
- make -j$(sysctl -n hw.ncpu) compaction_job_test compaction_service_test
- /usr/local/bin/timeout 60 ./compaction_job_test --gtest_filter=CompactionJobTest.ResultSerialization
- /usr/local/bin/timeout 60 ./compaction_service_test --gtest_filter=CompactionServiceTest.VerifyStats

Reviewed By: joshkang97, jaykorean

Differential Revision: D104130456

Pulled By: xingbowang

fbshipit-source-id: e9e92cb044253edbec0fc79b92c3efff53f01d8b
2026-05-07 04:20:13 -07:00
anand76 08d8122809 Update HISTORY.md for 11.2.1 2026-04-29 12:55:26 -07:00
Josh Kang 7be9ffc81a Fix range tombstone conversion + ingest sst and re-enable crash tests (#14654)
Summary:
Fixes a correctness bug in read-path range-tombstone synthesis when it races with `IngestExternalFile`. The synthesis path could insert a tombstone into the active memtable at a snapshot's sequence number, while ingestion installed an L0 SST at `LastSequence + 1` — a higher seqno than the synthesized tombstone. This breaks the main assumption of range tombstone reads that all lower levels have lower seqno.

The fix introduces a per-CF `port::RWMutex` (`ColumnFamilyData::ingest_sst_lock_`) plus a per-memtable `ingest_seqno_barrier_`. Ingestion takes the read lock and range tombstone synthesis **tries** to take a write lock.

If iterator lock is successful, then we have a new updated barrier seqno that we can validate the iterator seqno against. An added benefit is we no longer need to gate against empty memtable. This was originally added as an easy fix to prevent memtables from being inserted into while ingestion was happening.

## The bug, by example

`ReadPathRangeTombstoneTest.NewerPointInOlderFileStillVisible` (`db/db_iterator_test.cc:6929`):

1. L0 file `b@1, c@2, d@3`, then L0 file `Delete(b)4, Delete(c)5`.
2. Active memtable: `Put(z)6`. Snapshot taken at seq 6.
3. `IngestExternalFile({c → "vc_live"})` → installed at L0 with seq 7.
4. Iterator at snap 6 walks the deletion run and synthesizes `[b, d) @ seq 6` into the active memtable via `MemTable::AddLogicallyRedundantRangeTombstone`.
5. `Get("c")` at the latest snapshot: memtable returns covering tombstone (seq 6), `Version::Get` short-circuits, **never reads `c@7`** — returns `NotFound` instead of `"vc_live"`.

The invariant `Version::Get` relies on (memtable seqs ≥ any L0 seq for the same key) is broken because synthesis writes at the *snapshot's* seq while ingestion writes at `LastSequence + 1`.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14654

Test Plan:
- Updated regression test
- Re-enable crashtests and manually run

| Flavor | Jobs |
|---|---:|
| `fbcode_blackbox_crash_test` | 200 |
| `fbcode_whitebox_crash_test` | 30 |
| `fbcode_asan_blackbox_crash_test` | 30 |
| `fbcode_tsan_blackbox_crash_test` | 30 |
| `fbcode_crash_test_with_atomic_flush` | 30 |
| `fbcode_crash_test_with_wc_txn` | 30 |
| `fbcode_crash_test_with_ts` | 30 |
| **Total** | **380** |

Reviewed By: xingbowang

Differential Revision: D102044512

Pulled By: joshkang97

fbshipit-source-id: 0c69187595edc5a5fa80be24bffdba710a92e56e
2026-04-29 12:40:45 -07:00
anand76 1d37a88852 Update version for 11.2.1 2026-04-28 22:16:41 -07:00
Anand Ananthabhotla 7b0019a1b8 Gate file_open_metadata consumption on fast_sst_open option (#14676)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14676

When fast_sst_open is disabled, RocksDB was still passing previously-persisted
file_open_metadata from the MANIFEST to NewRandomAccessFile. This could cause
failures when the metadata becomes stale (e.g. expired filesystem credentials).

This change gates the consumption of file_open_metadata in
TableCache::GetTableReader on the fast_sst_open option. When fast_sst_open is
false, previously persisted metadata is ignored and not passed to the filesystem
via FileOptions::file_metadata.

The fast_sst_open flag is threaded from MutableDBOptions through VersionSet ->
ColumnFamilySet -> ColumnFamilyData -> TableCache at construction time, ensuring
the gate is active before any table readers are opened during recovery. Dynamic
changes via SetDBOptions are also propagated to all existing TableCache instances.

Reviewed By: mszeszko-meta, xingbowang

Differential Revision: D102735581

fbshipit-source-id: 9a2c4dc0644a2f65c36b2468605df57779e127cd
2026-04-28 22:11:41 -07:00
Xingbo Wang d3a28293a5 Revert temporary 11.1 remote compaction stats serialization workaround (#14663)
Summary:
There is a separate way to handle incompatibility. Revert this.

- revert the temporary workaround from https://github.com/facebook/rocksdb/issues/14656 that serialized `InternalStats::CompactionStats::counts` with the pre-11.2 compaction-reason count
- restore remote compaction stats serialization to use the full `CompactionReason::kNumOfReasons` array size again

## Context
https://github.com/facebook/rocksdb/issues/14656 temporarily shrank the serialized `counts` array to keep remote compaction metadata readable across the 11.1/11.2 boundary. This follow-up removes that special case because the compatibility issue is being handled separately.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14663

Test Plan: - Not run in this metadata-prep task

Reviewed By: jaykorean

Differential Revision: D102376258

Pulled By: xingbowang

fbshipit-source-id: 55693ea25a97b70de9d53b0d6eebcbd88a325b1c
2026-04-24 12:44:18 -07:00
Xingbo Wang 2b3cdc5764 Keep remote compaction stats serialization compatible with 11.1
Summary

Keep remote compaction stats serialization compatible with RocksDB 11.1.
Serialize InternalStats::CompactionStats::counts using the pre-11.2
compaction-reason count so remote compaction metadata remains readable
across the version boundary.
Preserve the existing formatting-only follow-up commit on the branch.

Needs a better mechanism to make OptionTypeInfo::Array ser/des friendly.
Will follow up after 11.2 release.
2026-04-22 16:31:17 -07:00
xingbowang 6c65d8046d Revert "Migrate fbcode coro references from folly/experimental to folly/coro (5/6 - fbcode a-m)"
This reverts commit bad2d5b0af.

This change broke internal build.
2026-04-22 06:08:53 -07:00
anand1976 2126b6e3ba Update HISTORY.md for 11.2.0 release 2026-04-20 11:54:02 -07:00
34 changed files with 475 additions and 158 deletions
+4 -4
View File
@@ -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
View File
@@ -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.
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
+10 -5
View File
@@ -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
View File
@@ -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();
+33
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+41
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+12 -2
View File
@@ -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
+1 -1
View File
@@ -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)
+24
View File
@@ -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_));
}
+2
View File
@@ -136,7 +136,9 @@ class RWMutex {
~RWMutex();
void ReadLock();
bool TryReadLock();
void WriteLock();
bool TryWriteLock();
void ReadUnlock();
void WriteUnlock();
void AssertHeld() const {}
+4
View File
@@ -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
View File
@@ -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.
+51
View File
@@ -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.