Skip to content

Commit b4a3612

Browse files
anand1976pdillinger
authored andcommitted
Fix incorrect MultiScan handling of range limit between files (facebook#14011)
Summary: This PR fixes a bug in how MultiScan handled a scan range limit falling in the key range between files. The bug was in LevelIterator, where Prepare() relied on FindFile to determine the lower bound file for the range limit. FindFile returns the smallest file index with `range.limit < file.largest_key`. However, that doesn't guarantee that the range overlaps the file, as the `range.limit` could be smaller than `file.smallest_key`. This also fixes a bug in BlockBasedTableIterator of Valid() returning true even if status() returned error. This was exposed by the previous bug. Pull Request resolved: facebook#14011 Test Plan: Add unit tests in db_iterator_test and table_test Reviewed By: cbi42 Differential Revision: D83496439 Pulled By: anand1976 fbshipit-source-id: a9d2d138d69d0c816d9f4160a984b273d00d683f
1 parent c44a283 commit b4a3612

6 files changed

Lines changed: 121 additions & 7 deletions

File tree

db/db_iterator_test.cc

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4149,6 +4149,7 @@ class DBMultiScanIteratorTest : public DBTestBase,
41494149
: DBTestBase("db_multi_scan_iterator_test", /*env_do_fsync=*/true) {}
41504150
};
41514151

4152+
// Param 0: ReadOptions::fill_cache
41524153
INSTANTIATE_TEST_CASE_P(DBMultiScanIteratorTest, DBMultiScanIteratorTest,
41534154
::testing::Bool());
41544155

@@ -4395,6 +4396,86 @@ TEST_P(DBMultiScanIteratorTest, FailureTest) {
43954396
iter.reset();
43964397
}
43974398

4399+
TEST_P(DBMultiScanIteratorTest, RangeBetweenFiles) {
4400+
auto options = CurrentOptions();
4401+
options.target_file_size_base = 100 << 10; // 20KB
4402+
options.compaction_style = kCompactionStyleUniversal;
4403+
options.num_levels = 50;
4404+
options.compression = kNoCompression;
4405+
DestroyAndReopen(options);
4406+
4407+
auto rnd = Random::GetTLSInstance();
4408+
// Write ~200KB data
4409+
for (int i = 0; i < 100; ++i) {
4410+
ASSERT_OK(Put(Key(i), rnd->RandomString(2 << 10)));
4411+
}
4412+
ASSERT_OK(Flush());
4413+
4414+
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
4415+
ASSERT_EQ(2, NumTableFilesAtLevel(49));
4416+
4417+
// Test with a scan range that overlaps an entire file, with upper bound
4418+
// between 2 files
4419+
std::vector<LiveFileMetaData> file_meta;
4420+
dbfull()->GetLiveFilesMetaData(&file_meta);
4421+
ASSERT_EQ(file_meta.size(), 2);
4422+
std::vector<std::string> key_ranges(4);
4423+
key_ranges[0] = file_meta[0].smallestkey;
4424+
key_ranges[1] = file_meta[0].largestkey + "0";
4425+
key_ranges[2] = file_meta[1].smallestkey + "0";
4426+
key_ranges[3] = file_meta[1].largestkey;
4427+
ReadOptions ro;
4428+
ro.fill_cache = GetParam();
4429+
MultiScanArgs scan_options(BytewiseComparator());
4430+
scan_options.insert(key_ranges[0], key_ranges[1]);
4431+
scan_options.insert(key_ranges[2], key_ranges[3]);
4432+
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
4433+
std::unique_ptr<MultiScan> iter =
4434+
dbfull()->NewMultiScan(ro, cfh, scan_options);
4435+
try {
4436+
for (auto range : *iter) {
4437+
for (auto it : range) {
4438+
ASSERT_GE(it.first.ToString(), key_ranges[0]);
4439+
}
4440+
}
4441+
} catch (MultiScanException& ex) {
4442+
// Make sure exception contains the status
4443+
ASSERT_NOK(ex.status());
4444+
std::cerr << "Iterator returned status " << ex.what();
4445+
abort();
4446+
} catch (std::logic_error& ex) {
4447+
std::cerr << "Iterator returned logic error " << ex.what();
4448+
abort();
4449+
}
4450+
iter.reset();
4451+
4452+
// Test multiscan with a range entirely between adjacent files
4453+
key_ranges[0] = file_meta[0].largestkey + "0";
4454+
key_ranges[1] = file_meta[0].largestkey + "1";
4455+
key_ranges[2] = file_meta[1].smallestkey + "0";
4456+
key_ranges[3] = file_meta[1].largestkey;
4457+
(*scan_options).clear();
4458+
scan_options.insert(key_ranges[0], key_ranges[1]);
4459+
scan_options.insert(key_ranges[2], key_ranges[3]);
4460+
iter = dbfull()->NewMultiScan(ro, cfh, scan_options);
4461+
try {
4462+
for (auto range : *iter) {
4463+
for (auto it : range) {
4464+
ASSERT_GE(it.first.ToString(), key_ranges[0]);
4465+
}
4466+
}
4467+
} catch (MultiScanException& ex) {
4468+
// Make sure exception contains the status
4469+
ASSERT_NOK(ex.status());
4470+
std::cerr << "Iterator returned status " << ex.what();
4471+
abort();
4472+
} catch (std::logic_error& ex) {
4473+
std::cerr << "Iterator returned logic error " << ex.what();
4474+
abort();
4475+
}
4476+
iter.reset();
4477+
}
4478+
43984479
} // namespace ROCKSDB_NAMESPACE
43994480

44004481
int main(int argc, char** argv) {

db/version_set.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,12 @@ class LevelIterator final : public InternalIterator {
11811181
// 3. [ S ] ...... [ E ]
11821182
for (auto i = fstart; i <= fend; i++) {
11831183
if (i < flevel_->num_files) {
1184+
// FindFile only compares against the largest_key, so we need this
1185+
// additional check to ensure the scan range overlaps the file
1186+
if (icomparator_.InternalKeyComparator::Compare(
1187+
iend.Encode(), flevel_->files[i].smallest_key) < 0) {
1188+
continue;
1189+
}
11841190
auto& args = GetMultiScanArgForFile(i);
11851191
args.insert(start.value(), end.value(), opt.property_bag);
11861192
}
@@ -1365,6 +1371,14 @@ void LevelIterator::Seek(const Slice& target) {
13651371
}
13661372

13671373
if (file_iter_.iter() != nullptr) {
1374+
if (scan_opts_) {
1375+
// At this point, we only know that the seek target is < largest_key
1376+
// in the file. We need to check whether there is actual overlap.
1377+
const FdWithKeyRange& cur_file = flevel_->files[file_index_];
1378+
if (KeyReachedUpperBound(cur_file.smallest_key)) {
1379+
return;
1380+
}
1381+
}
13681382
file_iter_.Seek(target);
13691383
// Status::TryAgain indicates asynchronous request for retrieval of data
13701384
// blocks has been submitted. So it should return at this point and Seek

table/block_based/block_based_table_iterator.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
5959
bool NextAndGetResult(IterateResult* result) override;
6060
void Prev() override;
6161
bool Valid() const override {
62-
return !is_out_of_bound_ &&
62+
return !is_out_of_bound_ && multi_scan_status_.ok() &&
6363
(is_at_first_key_from_index_ ||
6464
(block_iter_points_to_real_block_ && block_iter_.Valid()));
6565
}

table/table_test.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8464,6 +8464,22 @@ TEST_F(UserDefinedIndexTest, MultiScanFailureTest) {
84648464
iter->Seek(key_ranges[2]);
84658465
// Seek should fail as its not in the order specified in scan_options
84668466
ASSERT_EQ(iter->status(), Status::InvalidArgument());
8467+
ASSERT_FALSE(iter->Valid());
8468+
iter.reset();
8469+
8470+
iter.reset(db->NewIterator(ro, cfh));
8471+
ASSERT_NE(iter, nullptr);
8472+
scan_options.max_prefetch_size = 0;
8473+
iter->Prepare(scan_options);
8474+
ub = key_ranges[1];
8475+
iter->Seek(key_ranges[0]);
8476+
ASSERT_OK(iter->status()) << iter->status().ToString();
8477+
ASSERT_TRUE(iter->Valid());
8478+
ub = key_ranges[3];
8479+
iter->Seek("key13");
8480+
// Seek should fail as its not in the order specified in scan_options
8481+
ASSERT_EQ(iter->status(), Status::InvalidArgument());
8482+
ASSERT_FALSE(iter->Valid());
84678483
iter.reset();
84688484

84698485
iter.reset(db->NewIterator(ro, cfh));

tools/db_crashtest.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def setup_random_seed_before_main():
367367
"memtable_veirfy_per_key_checksum_on_seek": lambda: random.choice([0] * 7 + [1]),
368368
"allow_unprepared_value": lambda: random.choice([0, 1]),
369369
# TODO(hx235): enable `track_and_verify_wals` after stabalizing the stress test
370-
"track_and_verify_wals": lambda: random.choice([0]),
370+
"track_and_verify_wals": lambda: random.choice([0]),
371371
"remote_compaction_worker_threads": lambda: random.choice([0, 8]),
372372
# TODO(jaykorean): Change to lambda: random.choice([0, 1]) after addressing all remote compaction failures
373373
"remote_compaction_failure_fall_back_to_local": 1,
@@ -1155,13 +1155,15 @@ def finalize_and_sanitize(src_params):
11551155
# Continuous verification fails with secondaries inside NonBatchedOpsStressTest
11561156
if dest_params.get("test_secondary") == 1:
11571157
dest_params["continuous_verification_interval"] = 0
1158-
if (
1159-
dest_params.get("prefix_size", 0) > 0
1160-
or dest_params.get("read_fault_one_in", 0) > 0
1161-
):
1162-
dest_params["use_multiscan"] = 0
11631158
if dest_params.get("use_multiscan") == 1:
11641159
dest_params["async_io"] = 0
1160+
dest_params["delpercent"] += dest_params["delrangepercent"]
1161+
dest_params["delrangepercent"] = 0
1162+
dest_params["prefix_size"] = -1
1163+
dest_params["iterpercent"] += dest_params["prefixpercent"]
1164+
dest_params["prefixpercent"] = 0
1165+
dest_params["read_fault_one_in"] = 0
1166+
dest_params["memtable_prefix_bloom_size_ratio"] = 0
11651167
return dest_params
11661168

11671169

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix incorrect MultiScan seek error status due to bugs in handling range limit falling between adjacent SST files key range.

0 commit comments

Comments
 (0)