Skip to content

Commit 4b0558b

Browse files
cbi42Xingbo Wang
authored andcommitted
Fix handling of out-of-range scan option (facebook#13995)
Summary: currently BlockBasedTableIterator::Prepare() fails the iterator with non-ok status if an out-of-range scan option is detected. This is due to the interaction between LevelIterator and BlockBasedTableIterator, see added comment above BlockBasedTableIterator::Prepare(). This can fail stress test for L0 files since it doesn't use LevelIterator and scan options are not pruned. This PR fixes this by adding an internal option to MultiScanArgs that enables this check. Pull Request resolved: facebook#13995 Test Plan: - new unit test - stress test that fails before this pr: `python3 -u ./tools/db_crashtest.py whitebox --iterpercent=60 --prefix_size=-1 --prefixpercent=0 --readpercent=0 --test_batches_snapshots=0 --use_multiscan=1 --read_fault_one_in=0 --kill_random_test=88888 --interval=60 --multiscan_use_async_io=0 --mmap_read=0 --level0_file_num_compaction_trigger=20` Reviewed By: anand1976 Differential Revision: D83166088 Pulled By: cbi42 fbshipit-source-id: 241a7d43c8c00d9a98eea0cabb03d2174d51aae5
1 parent a74934f commit 4b0558b

6 files changed

Lines changed: 159 additions & 17 deletions

File tree

db/db_iterator_test.cc

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4476,6 +4476,74 @@ TEST_P(DBMultiScanIteratorTest, RangeBetweenFiles) {
44764476
iter.reset();
44774477
}
44784478

4479+
TEST_P(DBMultiScanIteratorTest, OutOfL0FileRange) {
4480+
// Test that prepare does not fail scan when a scan range
4481+
// is outside of a L0 file's key range.
4482+
auto options = CurrentOptions();
4483+
options.compression = kNoCompression;
4484+
DestroyAndReopen(options);
4485+
4486+
Random rnd(301);
4487+
// Create a Lmax file
4488+
// key01 ~ key99
4489+
for (int i = 0; i < 100; ++i) {
4490+
std::stringstream ss;
4491+
ss << std::setw(2) << std::setfill('0') << i;
4492+
ASSERT_OK(Put("k" + ss.str(), rnd.RandomString(1024)));
4493+
}
4494+
ASSERT_OK(Flush());
4495+
CompactRangeOptions cro;
4496+
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
4497+
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
4498+
4499+
// Create a L0 file
4500+
// key00 ~ key09
4501+
for (int i = 0; i < 10; ++i) {
4502+
std::stringstream ss;
4503+
ss << std::setw(2) << std::setfill('0') << i;
4504+
ASSERT_OK(Put("k" + ss.str(), rnd.RandomString(1024)));
4505+
}
4506+
ASSERT_OK(Flush());
4507+
ASSERT_EQ(NumTableFilesAtLevel(0), 1);
4508+
4509+
// The second range is outside of L0 file's key range
4510+
std::vector<std::string> key_ranges({"k04", "k06", "k12", "k14"});
4511+
ReadOptions ro;
4512+
Slice ub;
4513+
ro.iterate_upper_bound = &ub;
4514+
ro.fill_cache = GetParam();
4515+
MultiScanArgs scan_options(BytewiseComparator());
4516+
scan_options.insert(key_ranges[0], key_ranges[1]);
4517+
scan_options.insert(key_ranges[2], key_ranges[3]);
4518+
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
4519+
std::unique_ptr<Iterator> iter(dbfull()->NewIterator(ro, cfh));
4520+
ASSERT_NE(iter, nullptr);
4521+
iter->Prepare(scan_options);
4522+
int count = 0;
4523+
ub = key_ranges[1];
4524+
iter->Seek(key_ranges[0]);
4525+
while (iter->status().ok() && iter->Valid()) {
4526+
ASSERT_GE(iter->key().compare(key_ranges[0]), 0);
4527+
ASSERT_LT(iter->key().compare(key_ranges[1]), 0);
4528+
count++;
4529+
iter->Next();
4530+
}
4531+
ASSERT_OK(iter->status()) << iter->status().ToString();
4532+
ASSERT_EQ(count, 2);
4533+
4534+
ub = key_ranges[3];
4535+
count = 0;
4536+
iter->Seek(key_ranges[2]);
4537+
while (iter->status().ok() && iter->Valid()) {
4538+
ASSERT_GE(iter->key().compare(key_ranges[2]), 0);
4539+
ASSERT_LT(iter->key().compare(key_ranges[3]), 0);
4540+
count++;
4541+
iter->Next();
4542+
}
4543+
ASSERT_OK(iter->status()) << iter->status().ToString();
4544+
ASSERT_EQ(count, 2);
4545+
}
4546+
44794547
} // namespace ROCKSDB_NAMESPACE
44804548

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

db/version_set.cc

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,13 +1192,10 @@ class LevelIterator final : public InternalIterator {
11921192
}
11931193
}
11941194
}
1195-
// Propagate io colaescing threshold
1196-
// TODO: This is error prone as we may forget to copy some fields. Think
1197-
// of a better way to do this.
1195+
// Propagate multiscan configs
11981196
for (auto& file_to_arg : *file_to_scan_opts_) {
1199-
file_to_arg.second.io_coalesce_threshold = so->io_coalesce_threshold;
1200-
file_to_arg.second.max_prefetch_size = so->max_prefetch_size;
1201-
file_to_arg.second.use_async_io = so->use_async_io;
1197+
file_to_arg.second.CopyConfigFrom(*so);
1198+
file_to_arg.second.SetRequireFileOverlap(true);
12021199
}
12031200
}
12041201

include/rocksdb/options.h

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1848,7 +1848,7 @@ class MultiScanArgs {
18481848
operator std::vector<ScanOptions>*() { return &original_ranges_; }
18491849

18501850
operator const std::vector<ScanOptions>*() const { return &original_ranges_; }
1851-
// Destructor
1851+
18521852
~MultiScanArgs() {}
18531853

18541854
const std::vector<ScanOptions>& GetScanRanges() const {
@@ -1857,6 +1857,20 @@ 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+
1866+
// Copies the configurations (excluding actual scan ranges) from another
1867+
// MultiScanArgs.
1868+
void CopyConfigFrom(const MultiScanArgs& other) {
1869+
io_coalesce_threshold = other.io_coalesce_threshold;
1870+
max_prefetch_size = other.max_prefetch_size;
1871+
use_async_io = other.use_async_io;
1872+
}
1873+
18601874
uint64_t io_coalesce_threshold = 16 << 10; // 16KB by default
18611875

18621876
// Maximum size (in bytes) for the data blocks loaded by a MultiScan.
@@ -1880,6 +1894,11 @@ class MultiScanArgs {
18801894
// The comparator used for ordering ranges
18811895
const Comparator* comp_;
18821896
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};
18831902
};
18841903

18851904
// Options that control read operations

table/block_based/block_based_table_iterator.cc

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,15 @@ BlockBasedTableIterator::MultiScanState::~MultiScanState() {
963963
// - After Prepare(), the iterator expects Seek to be called on the start key
964964
// of each ScanOption in order. If any other Seek is done, an error status is
965965
// returned
966+
// - Whenever all blocks of a scan opt are exhausted, the iterator will become
967+
// invalid and UpperBoundCheckResult() will return kOutOfBound. So that the
968+
// upper layer (LevelIterator) will stop scanning instead thinking EOF is
969+
// reached and continue into the next file. The only exception is for the last
970+
// scan opt. If we reach the end of the last scan opt, UpperBoundCheckResult()
971+
// will return kUnknown instead of kOutOfBound. This mechanism requires that
972+
// 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.
966975
// FIXME: DBIter and MergingIterator may
967976
// internally do Seek() on child iterators, e.g. due to
968977
// ReadOptions::max_skippable_internal_keys or reseeking into range deletion
@@ -989,8 +998,9 @@ void BlockBasedTableIterator::Prepare(const MultiScanArgs* multiscan_opts) {
989998
std::vector<BlockHandle> scan_block_handles;
990999
std::vector<std::tuple<size_t, size_t>> block_index_ranges_per_scan;
9911000
const std::vector<ScanOptions>& scan_opts = multiscan_opts->GetScanRanges();
992-
multi_scan_status_ = CollectBlockHandles(scan_opts, &scan_block_handles,
993-
&block_index_ranges_per_scan);
1001+
multi_scan_status_ =
1002+
CollectBlockHandles(scan_opts, multiscan_opts->RequireFileOverlap(),
1003+
&scan_block_handles, &block_index_ranges_per_scan);
9941004
if (!multi_scan_status_.ok()) {
9951005
return;
9961006
}
@@ -1316,7 +1326,7 @@ Status BlockBasedTableIterator::ValidateScanOptions(
13161326
}
13171327

13181328
Status BlockBasedTableIterator::CollectBlockHandles(
1319-
const std::vector<ScanOptions>& scan_opts,
1329+
const std::vector<ScanOptions>& scan_opts, bool require_file_overlap,
13201330
std::vector<BlockHandle>* scan_block_handles,
13211331
std::vector<std::tuple<size_t, size_t>>* block_index_ranges_per_scan) {
13221332
for (const auto& scan_opt : scan_opts) {
@@ -1368,12 +1378,14 @@ Status BlockBasedTableIterator::CollectBlockHandles(
13681378
++num_blocks;
13691379
} else if (num_blocks == 0 && index_iter_->UpperBoundCheckResult() !=
13701380
IterBoundCheck::kOutOfBound) {
1371-
// We should not have scan ranges that are completely after the file's
1372-
// range. This is important for FindBlockForwardInMultiScan() which only
1373-
// lets the upper layer (LevelIterator) advance to the next SST file when
1374-
// the last scan range is exhausted.
1375-
return Status::InvalidArgument("Scan does not intersect with file");
1376-
;
1381+
// If require_file_overlap is set, then the scan ranges for this file
1382+
// must intersect with the file. Otherwise, allow empty intersection.
1383+
if (require_file_overlap) {
1384+
// This is important for FindBlockForwardInMultiScan() which only
1385+
// lets the upper layer (LevelIterator) advance to the next SST file
1386+
// when the last scan range is exhausted.
1387+
return Status::InvalidArgument("Scan does not intersect with file");
1388+
}
13771389
}
13781390
block_index_ranges_per_scan->emplace_back(
13791391
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
@@ -669,7 +669,7 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
669669
Status ValidateScanOptions(const MultiScanArgs* multiscan_opts);
670670

671671
Status CollectBlockHandles(
672-
const std::vector<ScanOptions>& scan_opts,
672+
const std::vector<ScanOptions>& scan_opts, bool require_file_overlap,
673673
std::vector<BlockHandle>* scan_block_handles,
674674
std::vector<std::tuple<size_t, size_t>>* block_index_ranges_per_scan);
675675

table/block_based/block_based_table_reader_test.cc

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,6 +1492,52 @@ TEST_P(BlockBasedTableReaderTest, MultiScanUnpinPreviousBlocks) {
14921492
}
14931493
}
14941494

1495+
TEST_P(BlockBasedTableReaderTest, MultiScanOptFileOverlapChecking) {
1496+
std::vector<std::pair<std::string, std::string>> kv =
1497+
BlockBasedTableReaderBaseTest::GenerateKVMap(
1498+
20 /* num_block */,
1499+
true /* mixed_with_human_readable_string_value */);
1500+
std::vector<std::pair<std::string, std::string>> actual_kv(
1501+
kv.begin(), kv.begin() + 15 * kEntriesPerBlock);
1502+
1503+
std::string table_name = "BlockBasedTableReaderTest_UnpinPreviousBlocks" +
1504+
CompressionTypeToString(compression_type_);
1505+
ImmutableOptions ioptions(options_);
1506+
CreateTable(table_name, ioptions, compression_type_, actual_kv,
1507+
compression_parallel_threads_, compression_dict_bytes_);
1508+
1509+
std::unique_ptr<BlockBasedTable> table;
1510+
FileOptions foptions;
1511+
foptions.use_direct_reads = use_direct_reads_;
1512+
InternalKeyComparator comparator(options_.comparator);
1513+
NewBlockBasedTableReader(foptions, ioptions, comparator, table_name, &table,
1514+
true /* bool prefetch_index_and_filter_in_cache */,
1515+
nullptr /* status */, persist_udt_);
1516+
1517+
ReadOptions read_opts;
1518+
std::unique_ptr<InternalIterator> iter;
1519+
iter.reset(table->NewIterator(
1520+
read_opts, options_.prefix_extractor.get(), /*arena=*/nullptr,
1521+
/*skip_filters=*/false, TableReaderCaller::kUncategorized));
1522+
1523+
MultiScanArgs scan_options(BytewiseComparator());
1524+
scan_options.SetRequireFileOverlap(false);
1525+
scan_options.insert(ExtractUserKey(kv[5 * kEntriesPerBlock].first),
1526+
ExtractUserKey(kv[6 * kEntriesPerBlock].first));
1527+
scan_options.insert(ExtractUserKey(kv[16 * kEntriesPerBlock].first),
1528+
ExtractUserKey(kv[17 * kEntriesPerBlock].first));
1529+
1530+
iter->Prepare(&scan_options);
1531+
ASSERT_OK(iter->status());
1532+
1533+
iter.reset(table->NewIterator(
1534+
read_opts, options_.prefix_extractor.get(), /*arena=*/nullptr,
1535+
/*skip_filters=*/false, TableReaderCaller::kUncategorized));
1536+
scan_options.SetRequireFileOverlap(true);
1537+
iter->Prepare(&scan_options);
1538+
ASSERT_TRUE(iter->status().IsInvalidArgument());
1539+
}
1540+
14951541
// Param 1: compression type
14961542
// Param 2: whether to use direct reads
14971543
// Param 3: Block Based Table Index type, partitioned filters are also enabled

0 commit comments

Comments
 (0)