Skip to content

Commit c874628

Browse files
anand1976Xingbo Wang
authored andcommitted
Allow empty MultiScan result in BlockBasedTableIterator Prepare (facebook#14046)
Summary: Currently in BlockBasedTableIterator's Prepare(), the index lookup for a MultiScan range is expected to return atleast 1 data block (unless UDI is in use). This is because there's an implicit assumption that only ranges intersecting with the keys in the file will be prepared. This assumption, however, doesn't hold if there are range deletions and the smallest and/or largest keys in the file extend beyond the keys in the file. The LevelIterator prunes the MultiScan ranges based on the smallest/largest key, so its possible for a range to only overlap the range deletion portion of the file and not overlap any of the data blocks. Furthermore, the BlockBasedTableIterator is now much more forgiving of Seek to targets outside of prepared ranges after facebook#14040 . Keeping the above in mind, this PR removes the check in BlockBasedTableIterator for non-empty index result. It adds assertions in LevelIterator to verify that ranges are being properly pruned. Another side effect is we can no longer rely solely on a scan range having 0 data blocks (i.e cur_scan_start_idx >= cur_scan_end_idx) to decide if the iterator is out of bound. We can only do so for all but the last range prepared range. Pull Request resolved: facebook#14046 Test Plan: 1. Add unit test in db_iterator_test 2. Run crash test Reviewed By: xingbowang Differential Revision: D84623871 Pulled By: anand1976 fbshipit-source-id: 2418e629f92b1c46c555ddea3761140f700819e4
1 parent 6507b9c commit c874628

6 files changed

Lines changed: 150 additions & 139 deletions

File tree

db/db_iterator_test.cc

Lines changed: 116 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4464,72 +4464,138 @@ TEST_P(DBMultiScanIteratorTest, RangeBetweenFiles) {
44644464
iter.reset();
44654465
}
44664466

4467-
TEST_P(DBMultiScanIteratorTest, OutOfL0FileRange) {
4468-
// Test that prepare does not fail scan when a scan range
4469-
// is outside of a L0 file's key range.
4467+
// This test case tests multiscan in the presence of fragmented range
4468+
// tombstones in the LSM.
4469+
TEST_P(DBMultiScanIteratorTest, FragmentedRangeTombstones) {
44704470
auto options = CurrentOptions();
4471+
// Compaction may create files 2x the target_file_size_base,
4472+
// so set this to 50KB so we atleast end up with 2 files of
4473+
// 100KB
4474+
options.target_file_size_base = 50 << 10; // 50KB
4475+
options.compaction_style = kCompactionStyleUniversal;
4476+
options.num_levels = 50;
44714477
options.compression = kNoCompression;
44724478
DestroyAndReopen(options);
44734479

4474-
Random rnd(301);
4475-
// Create a Lmax file
4476-
// key01 ~ key99
4477-
for (int i = 0; i < 100; ++i) {
4478-
std::stringstream ss;
4479-
ss << std::setw(2) << std::setfill('0') << i;
4480-
ASSERT_OK(Put("k" + ss.str(), rnd.RandomString(1024)));
4481-
}
4480+
// Setup the LSM as follows -
4481+
// 1. Ingest a file with 100 keys
4482+
// 2. Ingest a file with one overlapping key
4483+
// 3. Do a Put and flush a file to L0 with one overlapping key
4484+
// 4. Ingest a standalone delete range file that covers the full key space
4485+
// and a file with the same 100 keys with new values. This will ingest
4486+
// into L0 due to the presence of an existing file in L0
4487+
// The final LSM will have an SST in Lmax with 100 keys, and 2 SST files
4488+
// in Lmax-1 with half the keys each and completely overlapping delete ranges
4489+
std::unordered_map<std::string, std::string> kvs;
4490+
auto rnd = Random::GetTLSInstance();
4491+
auto create_ingestion_data_file_and_update_key_value =
4492+
[&](const std::string& filename, int start_key, int end_key) {
4493+
std::unique_ptr<SstFileWriter> writer;
4494+
writer.reset(new SstFileWriter(EnvOptions(), options));
4495+
ASSERT_OK(writer->Open(filename));
4496+
for (int i = start_key; i < end_key; ++i) {
4497+
auto kiter = kvs.find(Key(i));
4498+
if (kiter != kvs.end()) {
4499+
kvs.erase(kiter);
4500+
}
4501+
auto res =
4502+
kvs.emplace(std::make_pair(Key(i), rnd->RandomString(2 << 10)));
4503+
ASSERT_OK(writer->Put(res.first->first, res.first->second));
4504+
}
4505+
ASSERT_OK(writer->Finish());
4506+
writer.reset();
4507+
};
4508+
4509+
CreateColumnFamilies({"new_cf"}, options);
4510+
std::string ingest_file = dbname_ + "test.sst";
4511+
// Write ~200KB data
4512+
create_ingestion_data_file_and_update_key_value(ingest_file + "_0", 0, 100);
4513+
create_ingestion_data_file_and_update_key_value(ingest_file + "_1", 50, 51);
4514+
ColumnFamilyHandle* cfh = handles_[0];
4515+
IngestExternalFileOptions ifo;
4516+
Status s = dbfull()->IngestExternalFile(
4517+
cfh, {ingest_file + "_0", ingest_file + "_1"}, ifo);
4518+
ASSERT_OK(s);
4519+
4520+
ASSERT_OK(Put(0, Key(50), rnd->RandomString(2 << 10)));
44824521
ASSERT_OK(Flush());
4483-
CompactRangeOptions cro;
4484-
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
4485-
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
44864522

4487-
// Create a L0 file
4488-
// key00 ~ key09
4489-
for (int i = 0; i < 10; ++i) {
4490-
std::stringstream ss;
4491-
ss << std::setw(2) << std::setfill('0') << i;
4492-
ASSERT_OK(Put("k" + ss.str(), rnd.RandomString(1024)));
4523+
{
4524+
std::unique_ptr<SstFileWriter> writer;
4525+
writer.reset(new SstFileWriter(EnvOptions(), options));
4526+
ASSERT_OK(writer->Open(ingest_file + "_2"));
4527+
ASSERT_OK(writer->DeleteRange("a", "z"));
4528+
ASSERT_OK(writer->Finish());
4529+
writer.reset();
44934530
}
4494-
ASSERT_OK(Flush());
4495-
ASSERT_EQ(NumTableFilesAtLevel(0), 1);
4531+
create_ingestion_data_file_and_update_key_value(ingest_file + "_3", 0, 100);
4532+
s = dbfull()->IngestExternalFile(
4533+
cfh, {ingest_file + "_2", ingest_file + "_3"}, ifo);
4534+
ASSERT_OK(s);
44964535

4497-
// The second range is outside of L0 file's key range
4498-
std::vector<std::string> key_ranges({"k04", "k06", "k12", "k14"});
4536+
ASSERT_OK(dbfull()->TEST_WaitForCompact());
4537+
4538+
// The first scan range overlaps the DB key range, while the second extends
4539+
// beyond but overlaps the delete range
4540+
std::vector<std::string> key_ranges({"key000085", "key000090", "l", "n"});
44994541
ReadOptions ro;
4500-
Slice ub;
4501-
ro.iterate_upper_bound = &ub;
45024542
ro.fill_cache = GetParam();
45034543
MultiScanArgs scan_options(BytewiseComparator());
45044544
scan_options.insert(key_ranges[0], key_ranges[1]);
45054545
scan_options.insert(key_ranges[2], key_ranges[3]);
4506-
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
4507-
std::unique_ptr<Iterator> iter(dbfull()->NewIterator(ro, cfh));
4508-
ASSERT_NE(iter, nullptr);
4509-
iter->Prepare(scan_options);
4510-
int count = 0;
4511-
ub = key_ranges[1];
4512-
iter->Seek(key_ranges[0]);
4513-
while (iter->status().ok() && iter->Valid()) {
4514-
ASSERT_GE(iter->key().compare(key_ranges[0]), 0);
4515-
ASSERT_LT(iter->key().compare(key_ranges[1]), 0);
4516-
count++;
4517-
iter->Next();
4546+
std::unique_ptr<MultiScan> iter =
4547+
dbfull()->NewMultiScan(ro, cfh, scan_options);
4548+
try {
4549+
int i = 0;
4550+
int count = 0;
4551+
for (auto range : *iter) {
4552+
for (auto it : range) {
4553+
ASSERT_GE(it.first.ToString(), key_ranges[i]);
4554+
ASSERT_LT(it.first.ToString(), key_ranges[i + 1]);
4555+
auto kiter = kvs.find(it.first.ToString());
4556+
ASSERT_NE(kiter, kvs.end());
4557+
ASSERT_EQ(kiter->second, it.second.ToString());
4558+
count++;
4559+
}
4560+
i += 2;
4561+
}
4562+
ASSERT_EQ(i, 4);
4563+
ASSERT_EQ(count, 5);
4564+
} catch (MultiScanException& ex) {
4565+
ASSERT_OK(ex.status());
45184566
}
4519-
ASSERT_OK(iter->status()) << iter->status().ToString();
4520-
ASSERT_EQ(count, 2);
4567+
iter.reset();
45214568

4522-
ub = key_ranges[3];
4523-
count = 0;
4524-
iter->Seek(key_ranges[2]);
4525-
while (iter->status().ok() && iter->Valid()) {
4526-
ASSERT_GE(iter->key().compare(key_ranges[2]), 0);
4527-
ASSERT_LT(iter->key().compare(key_ranges[3]), 0);
4528-
count++;
4529-
iter->Next();
4569+
// The second scan range start overlaps the delete range in the first file
4570+
// in Lmax-1, while the end overlaps the keys in the second file
4571+
(*scan_options).clear();
4572+
key_ranges[0] = "key000010";
4573+
key_ranges[1] = "key000020";
4574+
key_ranges[2] = "key0000500";
4575+
key_ranges[3] = "key000060";
4576+
scan_options.insert(key_ranges[0], key_ranges[1]);
4577+
scan_options.insert(key_ranges[2], key_ranges[3]);
4578+
iter = dbfull()->NewMultiScan(ro, cfh, scan_options);
4579+
try {
4580+
int i = 0;
4581+
int count = 0;
4582+
for (auto range : *iter) {
4583+
for (auto it : range) {
4584+
ASSERT_GE(it.first.ToString(), key_ranges[i]);
4585+
ASSERT_LT(it.first.ToString(), key_ranges[i + 1]);
4586+
auto kiter = kvs.find(it.first.ToString());
4587+
ASSERT_NE(kiter, kvs.end());
4588+
ASSERT_EQ(kiter->second, it.second.ToString());
4589+
count++;
4590+
}
4591+
i += 2;
4592+
}
4593+
ASSERT_EQ(i, 4);
4594+
ASSERT_EQ(count, 19);
4595+
} catch (MultiScanException& ex) {
4596+
ASSERT_OK(ex.status());
45304597
}
4531-
ASSERT_OK(iter->status()) << iter->status().ToString();
4532-
ASSERT_EQ(count, 2);
4598+
iter.reset();
45334599
}
45344600

45354601
} // namespace ROCKSDB_NAMESPACE

db/version_set.cc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1200,7 +1200,6 @@ class LevelIterator final : public InternalIterator {
12001200
// Propagate multiscan configs
12011201
for (auto& file_to_arg : *file_to_scan_opts_) {
12021202
file_to_arg.second.CopyConfigFrom(*so);
1203-
file_to_arg.second.SetRequireFileOverlap(true);
12041203
}
12051204
}
12061205

@@ -1276,6 +1275,10 @@ class LevelIterator final : public InternalIterator {
12761275
}
12771276
}
12781277

1278+
#ifndef NDEBUG
1279+
bool OverlapRange(const ScanOptions& opts);
1280+
#endif
1281+
12791282
TableCache* table_cache_;
12801283
const ReadOptions& read_options_;
12811284
const FileOptions& file_options_;
@@ -1658,6 +1661,19 @@ void LevelIterator::SkipEmptyFileBackward() {
16581661
}
16591662
}
16601663

1664+
#ifndef NDEBUG
1665+
bool LevelIterator::OverlapRange(const ScanOptions& opts) {
1666+
return (user_comparator_.CompareWithoutTimestamp(
1667+
opts.range.start.value(), /*a_has_ts=*/false,
1668+
ExtractUserKey(flevel_->files[file_index_].largest_key),
1669+
/*b_has_ts=*/true) <= 0 &&
1670+
user_comparator_.CompareWithoutTimestamp(
1671+
opts.range.limit.value(), /*a_has_ts=*/false,
1672+
ExtractUserKey(flevel_->files[file_index_].smallest_key),
1673+
/*b_has_ts=*/true) > 0);
1674+
}
1675+
#endif
1676+
16611677
void LevelIterator::SetFileIterator(InternalIterator* iter) {
16621678
if (pinned_iters_mgr_ && iter) {
16631679
iter->SetPinnedItersMgr(pinned_iters_mgr_);
@@ -1667,6 +1683,8 @@ void LevelIterator::SetFileIterator(InternalIterator* iter) {
16671683
if (iter && scan_opts_) {
16681684
if (FileHasMultiScanArg(file_index_)) {
16691685
const MultiScanArgs& new_opts = GetMultiScanArgForFile(file_index_);
1686+
assert(OverlapRange(*new_opts.GetScanRanges().begin()) &&
1687+
OverlapRange(*new_opts.GetScanRanges().rbegin()));
16701688
file_iter_.Prepare(&new_opts);
16711689
}
16721690
}

include/rocksdb/options.h

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,12 +1857,6 @@ class MultiScanArgs {
18571857

18581858
const Comparator* GetComparator() const { return comp_; }
18591859

1860-
void SetRequireFileOverlap(bool require_overlap) {
1861-
require_file_overlap_ = require_overlap;
1862-
}
1863-
1864-
bool RequireFileOverlap() const { return require_file_overlap_; }
1865-
18661860
// Copies the configurations (excluding actual scan ranges) from another
18671861
// MultiScanArgs.
18681862
void CopyConfigFrom(const MultiScanArgs& other) {
@@ -1894,11 +1888,6 @@ class MultiScanArgs {
18941888
// The comparator used for ordering ranges
18951889
const Comparator* comp_;
18961890
std::vector<ScanOptions> original_ranges_;
1897-
1898-
// Internal use only.
1899-
// Fail the Prepare() on a file if a scan range does not overlap
1900-
// with the file.
1901-
bool require_file_overlap_{false};
19021891
};
19031892

19041893
// Options that control read operations

table/block_based/block_based_table_iterator.cc

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -970,13 +970,12 @@ BlockBasedTableIterator::MultiScanState::~MultiScanState() {
970970
// scan opt. If we reach the end of the last scan opt, UpperBoundCheckResult()
971971
// will return kUnknown instead of kOutOfBound. This mechanism requires that
972972
// scan opts are properly pruned such that there is no scan opt that is after
973-
// this file's key range. This check can be enforeced by setting
974-
// MultiScanArgs::require_file_overlap to true.
973+
// this file's key range.
975974
// FIXME: DBIter and MergingIterator may
976975
// internally do Seek() on child iterators, e.g. due to
977976
// ReadOptions::max_skippable_internal_keys or reseeking into range deletion
978-
// end key. So these Seeks can cause iterator to fall back to normal
979-
// (non-prepared) iterator and ignore the optimizations done in Prepare().
977+
// end key. These Seeks will be handled properly, as long as the target is
978+
// moving forward.
980979
void BlockBasedTableIterator::Prepare(const MultiScanArgs* multiscan_opts) {
981980
assert(!multi_scan_);
982981
if (!index_iter_->status().ok()) {
@@ -995,9 +994,9 @@ void BlockBasedTableIterator::Prepare(const MultiScanArgs* multiscan_opts) {
995994
std::vector<std::string> data_block_separators;
996995
std::vector<std::tuple<size_t, size_t>> block_index_ranges_per_scan;
997996
const std::vector<ScanOptions>& scan_opts = multiscan_opts->GetScanRanges();
998-
multi_scan_status_ = CollectBlockHandles(
999-
scan_opts, multiscan_opts->RequireFileOverlap(), &scan_block_handles,
1000-
&block_index_ranges_per_scan, &data_block_separators);
997+
multi_scan_status_ =
998+
CollectBlockHandles(scan_opts, &scan_block_handles,
999+
&block_index_ranges_per_scan, &data_block_separators);
10011000
if (!multi_scan_status_.ok()) {
10021001
return;
10031002
}
@@ -1168,7 +1167,13 @@ bool BlockBasedTableIterator::SeekMultiScanImpl(const Slice* seek_target) {
11681167
// We should have the data block already loaded
11691168
++multi_scan_->next_scan_idx;
11701169
if (cur_scan_start_idx >= cur_scan_end_idx) {
1171-
return out_of_bound;
1170+
if (multi_scan_->next_scan_idx <
1171+
multi_scan_->block_index_ranges_per_scan.size()) {
1172+
return out_of_bound;
1173+
} else {
1174+
ResetDataIter();
1175+
return false;
1176+
}
11721177
} else {
11731178
is_out_of_bound_ = false;
11741179
}
@@ -1418,7 +1423,7 @@ Status BlockBasedTableIterator::CreateAndPinBlockFromBuffer(
14181423
constexpr auto kVerbose = false;
14191424

14201425
Status BlockBasedTableIterator::CollectBlockHandles(
1421-
const std::vector<ScanOptions>& scan_opts, bool require_file_overlap,
1426+
const std::vector<ScanOptions>& scan_opts,
14221427
std::vector<BlockHandle>* scan_block_handles,
14231428
std::vector<std::tuple<size_t, size_t>>* block_index_ranges_per_scan,
14241429
std::vector<std::string>* data_block_separators) {
@@ -1481,16 +1486,6 @@ Status BlockBasedTableIterator::CollectBlockHandles(
14811486
data_block_separators->push_back(index_iter_->user_key().ToString());
14821487
}
14831488
++num_blocks;
1484-
} else if (num_blocks == 0 && index_iter_->UpperBoundCheckResult() !=
1485-
IterBoundCheck::kOutOfBound) {
1486-
// If require_file_overlap is set, then the scan ranges for this file
1487-
// must intersect with the file. Otherwise, allow empty intersection.
1488-
if (require_file_overlap) {
1489-
// This is important for FindBlockForwardInMultiScan() which only
1490-
// lets the upper layer (LevelIterator) advance to the next SST file
1491-
// when the last scan range is exhausted.
1492-
return Status::InvalidArgument("Scan does not intersect with file");
1493-
}
14941489
}
14951490
block_index_ranges_per_scan->emplace_back(
14961491
scan_block_handles->size() - num_blocks, scan_block_handles->size());

table/block_based/block_based_table_iterator.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
698698
CachableEntry<Block>& pinned_block_entry);
699699

700700
Status CollectBlockHandles(
701-
const std::vector<ScanOptions>& scan_opts, bool require_file_overlap,
701+
const std::vector<ScanOptions>& scan_opts,
702702
std::vector<BlockHandle>* scan_block_handles,
703703
std::vector<std::tuple<size_t, size_t>>* block_index_ranges_per_scan,
704704
std::vector<std::string>* data_block_boundary_keys);

0 commit comments

Comments
 (0)