Skip to content

Commit fba76b2

Browse files
Xingbo Wanganand1976
authored andcommitted
Fix a nullptr access bug in MultiScan (facebook#14062)
Summary: Fixing a nullptr access in multiscan, under following situation. ``` Block Based Table: blk1:[k1,k2], blk2:[k3, k8], blk3:[k9] Scan ranges: [k1, k4), [k5,k6), [k7, k10) Prepared block ranges: [0,2], [2,2], [1,3] ``` 1. Seek key k1 on the first range, read key k1, k2. 2. Seek key k4 on the 2nd range, blocks 0,1 would be unpinned. 3. Seek key k9, block 1 would be accessed, but it is unpinned, which trigger assert failure in debug mode and nullptr access on release build. This fix changes how blocks are unpinned. It is now only unpinning the block, when the cur_data_block_idx has passed it. Pull Request resolved: facebook#14062 Test Plan: Unit Test rand_seed 304010984 on UserDefinedIndexStressTest Reviewed By: cbi42 Differential Revision: D84976410 Pulled By: xingbowang fbshipit-source-id: 6b99bf85fc9d4108c5267ae77be77ccfe08923cd
1 parent 24efab2 commit fba76b2

4 files changed

Lines changed: 226 additions & 60 deletions

File tree

table/block_based/block_based_table_iterator.cc

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,10 +1157,6 @@ bool BlockBasedTableIterator::SeekMultiScanImpl(const Slice* seek_target) {
11571157
// Seeking a range that is out side of prepared ranges.
11581158
return out_of_bound;
11591159
}
1160-
// unpin block, then do a seek.
1161-
if (multi_scan_->next_scan_idx > 0) {
1162-
UnpinPreviousScanBlocks(multi_scan_->next_scan_idx);
1163-
}
11641160

11651161
auto [cur_scan_start_idx, cur_scan_end_idx] =
11661162
multi_scan_->block_index_ranges_per_scan[multi_scan_->next_scan_idx];
@@ -1231,6 +1227,8 @@ void BlockBasedTableIterator::MultiScanUnexpectedSeekTarget(
12311227

12321228
void BlockBasedTableIterator::MultiScanSeekTargetFromBlock(
12331229
const Slice* seek_target, size_t block_idx) {
1230+
assert(multi_scan_->cur_data_block_idx <= block_idx);
1231+
12341232
if (!block_iter_points_to_real_block_ ||
12351233
multi_scan_->cur_data_block_idx != block_idx) {
12361234
if (block_iter_points_to_real_block_) {
@@ -1245,32 +1243,20 @@ void BlockBasedTableIterator::MultiScanSeekTargetFromBlock(
12451243
return;
12461244
}
12471245
}
1248-
multi_scan_->cur_data_block_idx = block_idx;
1249-
block_iter_points_to_real_block_ = true;
1250-
block_iter_.Seek(*seek_target);
1251-
FindKeyForward();
1252-
}
12531246

1254-
void BlockBasedTableIterator::UnpinPreviousScanBlocks(size_t current_scan_idx) {
1255-
// TODO: support aborting and clearn up async IO requests, currently
1256-
// only unpins already initialized blocks
1257-
assert(multi_scan_);
1258-
assert(current_scan_idx < multi_scan_->block_index_ranges_per_scan.size());
1259-
if (current_scan_idx == 0) return;
1260-
1261-
auto prev_start_block_idx = std::get<0>(
1262-
multi_scan_->block_index_ranges_per_scan[current_scan_idx - 1]);
1263-
// Since a block can be shared between consecutive scans, we need
1264-
// curr_start_block_idx here instead of just release blocks
1265-
// up to the end of previous range block index.
1266-
auto curr_start_block_idx =
1267-
std::get<0>(multi_scan_->block_index_ranges_per_scan[current_scan_idx]);
1268-
for (size_t block_idx = prev_start_block_idx;
1269-
block_idx < curr_start_block_idx; ++block_idx) {
1270-
if (!multi_scan_->pinned_data_blocks[block_idx].IsEmpty()) {
1271-
multi_scan_->pinned_data_blocks[block_idx].Reset();
1247+
// Move current data block index forward until block_idx, meantime, unpin all
1248+
// the blocks in between
1249+
while (multi_scan_->cur_data_block_idx < block_idx) {
1250+
// unpin block
1251+
if (!multi_scan_->pinned_data_blocks[multi_scan_->cur_data_block_idx]
1252+
.IsEmpty()) {
1253+
multi_scan_->pinned_data_blocks[multi_scan_->cur_data_block_idx].Reset();
12721254
}
1255+
multi_scan_->cur_data_block_idx++;
12731256
}
1257+
block_iter_points_to_real_block_ = true;
1258+
block_iter_.Seek(*seek_target);
1259+
FindKeyForward();
12741260
}
12751261

12761262
void BlockBasedTableIterator::FindBlockForwardInMultiScan() {
@@ -1307,6 +1293,11 @@ void BlockBasedTableIterator::FindBlockForwardInMultiScan() {
13071293
}
13081294
// Move to the next pinned data block
13091295
ResetDataIter();
1296+
// Unpin previous block if it is not reset by data iterator
1297+
if (!multi_scan_->pinned_data_blocks[multi_scan_->cur_data_block_idx]
1298+
.IsEmpty()) {
1299+
multi_scan_->pinned_data_blocks[multi_scan_->cur_data_block_idx].Reset();
1300+
}
13101301
++multi_scan_->cur_data_block_idx;
13111302

13121303
if (MultiScanLoadDataBlock(multi_scan_->cur_data_block_idx)) {

table/block_based/block_based_table_iterator.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -620,9 +620,6 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
620620

621621
void FindBlockForwardInMultiScan();
622622

623-
// Unpins blocks from the immediately previous scan range.
624-
void UnpinPreviousScanBlocks(size_t current_scan_idx);
625-
626623
void PrepareReadAsyncCallBack(FSReadRequest& req, void* cb_arg) {
627624
// Record status, result and sanity check offset from `req`.
628625
AsyncReadState* async_state = static_cast<AsyncReadState*>(cb_arg);

table/table_test.cc

Lines changed: 207 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ namespace ROCKSDB_NAMESPACE {
8787
namespace {
8888

8989
const std::string kDummyValue(10000, 'o');
90+
constexpr auto kVerbose = false;
9091

9192
// DummyPropertiesCollector used to test BlockBasedTableProperties
9293
class DummyPropertiesCollector : public TablePropertiesCollector {
@@ -934,7 +935,6 @@ class HarnessTest : public testing::Test {
934935

935936
void TestRandomAccess(Random* rnd, const std::vector<std::string>& keys,
936937
const stl_wrappers::KVMap& data) {
937-
static const bool kVerbose = false;
938938
InternalIterator* iter = constructor_->NewIterator();
939939
ASSERT_TRUE(!iter->Valid());
940940
stl_wrappers::KVMap::const_iterator model_iter = data.begin();
@@ -7827,7 +7827,6 @@ class UserDefinedIndexTestBase : public BlockBasedTableTestBase {
78277827
read_opts.iterate_upper_bound = &ub;
78287828
std::unique_ptr<Iterator> iter(db->NewIterator(read_opts, cfh));
78297829
iter->Prepare(scan_opts);
7830-
static const bool kVerbose = false;
78317830
for (auto opt : opts) {
78327831
ub = opt.range.limit.value();
78337832
iter->Seek(opt.range.start.value());
@@ -8979,8 +8978,6 @@ std::ostream& operator<<(std::ostream& os,
89798978
<< param.enable_compaction_with_sst_partitioner << "}";
89808979
}
89818980

8982-
constexpr auto kVerbose = false;
8983-
89848981
struct DataRange {
89858982
size_t start; // inclusive
89868983
size_t end; // exclusive
@@ -9131,23 +9128,47 @@ class UserDefinedIndexStressTest
91319128
}
91329129

91339130
void CreateSstFileWithRanges(const std::string& ingest_file,
9134-
const DataRange& range) {
9135-
std::unique_ptr<SstFileWriter> writer =
9136-
std::make_unique<SstFileWriter>(EnvOptions(), options_);
9137-
ASSERT_OK(writer->Open(ingest_file));
9131+
const std::vector<DataRange>& ranges,
9132+
bool& data_added) {
9133+
std::unique_ptr<SstFileWriter> writer;
9134+
data_added = false;
91389135

9139-
assert(range.start != range.end);
9136+
std::vector<DataRange> ranges_in_file;
91409137

9141-
if (range.is_range_delete) {
9142-
ASSERT_OK(writer->DeleteRange(range.start_key, range.end_key));
9143-
} else {
9144-
for (size_t i = range.start; i != range.end;) {
9145-
auto key = FormatKey(i);
9146-
range.start < range.end ? i++ : i--;
9147-
ASSERT_OK(writer->Put(key, range.value));
9138+
for (auto const& range : ranges) {
9139+
assert(range.start != range.end);
9140+
if (range.skipped) {
9141+
continue;
9142+
}
9143+
9144+
if (writer == nullptr) {
9145+
writer = std::make_unique<SstFileWriter>(EnvOptions(), options_);
9146+
ASSERT_OK(writer->Open(ingest_file));
9147+
}
9148+
ranges_in_file.push_back(range);
9149+
9150+
data_added = true;
9151+
9152+
if (range.is_range_delete) {
9153+
ASSERT_OK(writer->DeleteRange(range.start_key, range.end_key));
9154+
} else {
9155+
for (size_t i = range.start; i != range.end;) {
9156+
auto key = FormatKey(i);
9157+
range.start < range.end ? i++ : i--;
9158+
ASSERT_OK(writer->Put(key, range.value));
9159+
}
9160+
}
9161+
}
9162+
if (kVerbose) {
9163+
std::cout << "Ingested file: " + ingest_file + "; Range: {" << std::endl;
9164+
for (const auto& range : ranges_in_file) {
9165+
std::cout << " " << range.ToString() << "," << std::endl;
91489166
}
9167+
std::cout << "}" << std::endl;
9168+
}
9169+
if (data_added) {
9170+
ASSERT_OK(writer->Finish());
91499171
}
9150-
ASSERT_OK(writer->Finish()) << range.ToString();
91519172
}
91529173

91539174
void RangeScan(std::unique_ptr<Iterator>& iter,
@@ -9267,17 +9288,42 @@ class UserDefinedIndexStressTest
92679288
void IngestFilesInOneLevel(const std::vector<DataRange>& ranges_in_level,
92689289
const std::string& ingest_file_name_prefix,
92699290
size_t& ingest_file_count,
9270-
const IngestExternalFileOptions& ifo) {
9291+
const IngestExternalFileOptions& ifo,
9292+
bool combine_ranges = false) {
92719293
std::vector<std::string> ingest_files;
92729294
// Generate SST file and bulk load them one level at a time
9273-
for (auto const& range : ranges_in_level) {
9274-
if (!range.skipped) {
9295+
if (combine_ranges) {
9296+
size_t i = 0;
9297+
while (i < ranges_in_level.size()) {
9298+
// if combine ranges, generate 1 SST file that combines muliple ranges
9299+
// together
9300+
size_t batch_end_idx =
9301+
std::min(i + rnd.Uniform(3) + 2, ranges_in_level.size());
9302+
bool data_added = false;
92759303
ASSERT_NO_FATAL_FAILURE(CreateSstFileWithRanges(
92769304
ingest_file_name_prefix + std::to_string(ingest_file_count),
9277-
range));
9278-
ingest_files.push_back(ingest_file_name_prefix +
9279-
std::to_string(ingest_file_count));
9280-
ingest_file_count++;
9305+
{ranges_in_level.begin() + i,
9306+
ranges_in_level.begin() + batch_end_idx},
9307+
data_added));
9308+
if (data_added) {
9309+
ingest_files.push_back(ingest_file_name_prefix +
9310+
std::to_string(ingest_file_count));
9311+
ingest_file_count++;
9312+
}
9313+
i = batch_end_idx;
9314+
}
9315+
} else {
9316+
for (auto const& range : ranges_in_level) {
9317+
if (!range.skipped) {
9318+
bool data_added = false;
9319+
ASSERT_NO_FATAL_FAILURE(CreateSstFileWithRanges(
9320+
ingest_file_name_prefix + std::to_string(ingest_file_count),
9321+
{range}, data_added));
9322+
ASSERT_TRUE(data_added);
9323+
ingest_files.push_back(ingest_file_name_prefix +
9324+
std::to_string(ingest_file_count));
9325+
ingest_file_count++;
9326+
}
92819327
}
92829328
}
92839329

@@ -9314,9 +9360,9 @@ class UserDefinedIndexStressTest
93149360
// Becuase query count == 2, level n+1 would only prepare 3-5. but since 4-6
93159361
// got deleted in the upper level, they are not returned, so only 3 is
93169362
// returned. Meantime the query should have return [3, 6]
9317-
// One way to fix this is by preparing more data blocks once prepared blocks are
9318-
// exhausted, but upper bound is not reached yet.
9319-
// This requires following changes:
9363+
// One way to fix this is by preparing more data blocks once prepared blocks
9364+
// are exhausted, but upper bound is not reached yet. This requires following
9365+
// changes:
93209366
// 1. Fix out of bound flag in block table iterator. Only set it if the key is
93219367
// larger than the upper bound.
93229368
// 2. Refactor the prepared block single dimension vector into 2 dimension of
@@ -9349,12 +9395,126 @@ TEST_P(UserDefinedIndexStressTest, DISABLED_PartialDeleteRange) {
93499395
ASSERT_NO_FATAL_FAILURE(ValidateQueryResult());
93509396
}
93519397

9398+
TEST_P(UserDefinedIndexStressTest, DeleteRangeMixedWithDataFile) {
9399+
// Create 2 column families. One use normal put/del, the other uses sst
9400+
// ingest.
9401+
// Test the case where there are 3 levels, the middle level is a delete
9402+
// range file that span across the entire key space. The top level file have
9403+
// multiple files and each one has both data and delete range Scan same
9404+
// range between the 2 CF and validate the result is same
9405+
SCOPED_TRACE("Start with random seed: " + std::to_string(rand_seed_));
9406+
dbname_ = test::PerThreadDBPath(
9407+
"UserDefinedIndexStressTest_DeleteRangeMixedWithDataFile");
9408+
SCOPED_TRACE("dbname: " + dbname_);
9409+
ASSERT_NO_FATAL_FAILURE(SetupDB(dbname_));
9410+
9411+
// Test 3 levels.
9412+
// bottom level is normal data files.
9413+
ranges_in_levels_.push_back(GenerateKeyRanges(rnd.Uniform(3) + 4, 2, "L6"));
9414+
// middle level delete range between each level
9415+
if (is_reverse_comparator_) {
9416+
ranges_in_levels_.push_back({{.start = 100,
9417+
.end = 0,
9418+
.is_range_delete = true,
9419+
.skipped = false,
9420+
.start_key = "keyz",
9421+
.end_key = "key"}});
9422+
} else {
9423+
ranges_in_levels_.push_back({{.start = 0,
9424+
.end = 100,
9425+
.is_range_delete = true,
9426+
.skipped = false,
9427+
.start_key = "key",
9428+
.end_key = "keyz"}});
9429+
}
9430+
9431+
// Top level is normal data files
9432+
ranges_in_levels_.push_back(GenerateKeyRanges(rnd.Uniform(3) + 4, 2, "L4"));
9433+
9434+
IngestExternalFileOptions ifo;
9435+
ifo.snapshot_consistency = false;
9436+
auto ingest_file_name_prefix = dbname_ + "ingest_file_";
9437+
size_t ingest_file_count = 0;
9438+
auto first_level = true;
9439+
for (auto const& ranges_in_level : ranges_in_levels_) {
9440+
ASSERT_NO_FATAL_FAILURE(
9441+
IngestFilesInOneLevel(ranges_in_level, ingest_file_name_prefix,
9442+
ingest_file_count, ifo, true));
9443+
if (first_level) {
9444+
first_level = false;
9445+
if (enable_compaction_with_sst_partitioner_) {
9446+
// When compaction is enabled, do a compaction at the first level
9447+
ASSERT_NO_FATAL_FAILURE(CompactIngestedCF());
9448+
}
9449+
}
9450+
}
9451+
9452+
ASSERT_NO_FATAL_FAILURE(AddDataToRegularCF());
9453+
9454+
ASSERT_NO_FATAL_FAILURE(ValidateQueryResult());
9455+
}
9456+
93529457
TEST_P(UserDefinedIndexStressTest, DeleteRange) {
93539458
// Create 2 column families. One use normal put/del, the other uses sst
93549459
// ingest.
9355-
// Test the case where there are 3 levels, the middle level is a delete range
9356-
// file that span across the entire key space.
9357-
// Range scan same range between the 2 CF and validate the result is same
9460+
// Test the case where there are 3 levels, the middle level is a delete
9461+
// range file that span across the entire key space. Range scan same range
9462+
// between the 2 CF and validate the result is same
9463+
SCOPED_TRACE("Start with random seed: " + std::to_string(rand_seed_));
9464+
dbname_ = test::PerThreadDBPath("UserDefinedIndexStressTest_DeleteRange");
9465+
SCOPED_TRACE("dbname: " + dbname_);
9466+
ASSERT_NO_FATAL_FAILURE(SetupDB(dbname_));
9467+
9468+
// Test 3 levels.
9469+
// bottom level is normal data files.
9470+
ranges_in_levels_.push_back(GenerateKeyRanges(rnd.Uniform(3) + 4, 2, "L6"));
9471+
// middle level delete range between each level
9472+
if (is_reverse_comparator_) {
9473+
ranges_in_levels_.push_back({{.start = 100,
9474+
.end = 0,
9475+
.is_range_delete = true,
9476+
.skipped = false,
9477+
.start_key = "keyz",
9478+
.end_key = "key"}});
9479+
} else {
9480+
ranges_in_levels_.push_back({{.start = 0,
9481+
.end = 100,
9482+
.is_range_delete = true,
9483+
.skipped = false,
9484+
.start_key = "key",
9485+
.end_key = "keyz"}});
9486+
}
9487+
// Top level is normal data files
9488+
ranges_in_levels_.push_back(GenerateKeyRanges(rnd.Uniform(3) + 4, 2, "L4"));
9489+
9490+
IngestExternalFileOptions ifo;
9491+
ifo.snapshot_consistency = false;
9492+
auto ingest_file_name_prefix = dbname_ + "ingest_file_";
9493+
size_t ingest_file_count = 0;
9494+
auto first_level = true;
9495+
for (auto const& ranges_in_level : ranges_in_levels_) {
9496+
ASSERT_NO_FATAL_FAILURE(IngestFilesInOneLevel(
9497+
ranges_in_level, ingest_file_name_prefix, ingest_file_count, ifo));
9498+
if (first_level) {
9499+
first_level = false;
9500+
if (enable_compaction_with_sst_partitioner_) {
9501+
// When compaction is enabled, do a compaction at the first level
9502+
ASSERT_NO_FATAL_FAILURE(CompactIngestedCF());
9503+
}
9504+
}
9505+
}
9506+
9507+
ASSERT_NO_FATAL_FAILURE(AddDataToRegularCF());
9508+
9509+
ASSERT_NO_FATAL_FAILURE(ValidateQueryResult());
9510+
}
9511+
9512+
TEST_P(UserDefinedIndexStressTest, AtomicReplaceBulkLoad) {
9513+
// Create 2 column families. One use normal put/del, the other uses sst
9514+
// ingest.
9515+
// Test the case where there are 3 levels, the middle level is a delete
9516+
// range file that span across the entire key space. Range scan same range
9517+
// between the 2 CF and validate the result is same
93589518
SCOPED_TRACE("Start with random seed: " + std::to_string(rand_seed_));
93599519
dbname_ = test::PerThreadDBPath("UserDefinedIndexStressTest_DeleteRange");
93609520
SCOPED_TRACE("dbname: " + dbname_);
@@ -9399,6 +9559,23 @@ TEST_P(UserDefinedIndexStressTest, DeleteRange) {
93999559
}
94009560
}
94019561

9562+
// Ingest the a new file with atomic replace with full key space, this layer
9563+
// is exactly same as the one at Level 4
9564+
bool data_added;
9565+
ASSERT_NO_FATAL_FAILURE(CreateSstFileWithRanges(
9566+
ingest_file_name_prefix + std::to_string(++ingest_file_count),
9567+
ranges_in_levels_[2], data_added));
9568+
9569+
IngestExternalFileArg ingest_arg;
9570+
ingest_arg.column_family = ingest_cfh_;
9571+
ingest_arg.options = ifo;
9572+
ingest_arg.external_files.push_back(ingest_file_name_prefix +
9573+
std::to_string(ingest_file_count));
9574+
ingest_arg.atomic_replace_range = RangeOpt(nullptr, nullptr);
9575+
9576+
ASSERT_OK(db_->IngestExternalFiles(
9577+
std::vector<IngestExternalFileArg>({ingest_arg})));
9578+
94029579
ASSERT_NO_FATAL_FAILURE(AddDataToRegularCF());
94039580

94049581
ASSERT_NO_FATAL_FAILURE(ValidateQueryResult());
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug in Page unpinning in MultiScan

0 commit comments

Comments
 (0)