Skip to content

Commit dc129cc

Browse files
luohahaluohahaclaude
authored
[UT] Add mixed upsert/delete op to LakePrimaryKeyConsistencyTest (#76090)
Signed-off-by: luohaha <casey.luo@celerdata.com> Co-authored-by: luohaha <casey.luo@celerdata.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2955bd0 commit dc129cc

1 file changed

Lines changed: 107 additions & 1 deletion

File tree

be/test/storage/lake/lake_primary_key_consistency_test.cpp

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ enum PICT_OP {
6060
PARTIAL_UPDATE_ROW = 6,
6161
PARTIAL_UPDATE_COLUMN = 7,
6262
CONDITION_UPDATE = 8,
63-
MAX = 9,
63+
MIXED_UPSERT_DELETE = 9,
64+
MAX = 10,
6465
};
6566

6667
static const std::string kTestGroupPath = "./test_lake_primary_key_consistency";
@@ -255,6 +256,7 @@ class LakePrimaryKeyConsistencyTest : public TestBase, testing::WithParamInterfa
255256
items.emplace_back(PICT_OP::PARTIAL_UPDATE_ROW, 5);
256257
items.emplace_back(PICT_OP::PARTIAL_UPDATE_COLUMN, 5);
257258
items.emplace_back(PICT_OP::CONDITION_UPDATE, 5);
259+
items.emplace_back(PICT_OP::MIXED_UPSERT_DELETE, 17);
258260
_random_op_selector = std::make_unique<WeightedRandomOpSelector<int, PICT_OP>>(_random_generator.get(), items);
259261
_io_failure_generator =
260262
std::make_unique<IOFailureGenerator<int, PICT_OP>>(_random_generator.get(), io_failure_percent);
@@ -283,9 +285,15 @@ class LakePrimaryKeyConsistencyTest : public TestBase, testing::WithParamInterfa
283285
_old_pk_index_parallel_compaction_task_split_threshold_bytes =
284286
config::pk_index_parallel_compaction_task_split_threshold_bytes;
285287
config::pk_index_parallel_compaction_task_split_threshold_bytes = 4 * 1024 * 1024;
288+
// Preserve in-transaction upsert/delete order so the Replayer (which replays ops in write order)
289+
// matches the tablet -- required for the interleaved mixed_upsert_delete_op (delete-then-upsert).
290+
// Both the serial (non-spill) and op-aware spill merge paths honor it.
291+
_old_lake_enable_pk_preserve_txn_delete_order = config::lake_enable_pk_preserve_txn_delete_order;
292+
config::lake_enable_pk_preserve_txn_delete_order = true;
286293
}
287294

288295
void TearDown() override {
296+
config::lake_enable_pk_preserve_txn_delete_order = _old_lake_enable_pk_preserve_txn_delete_order;
289297
(void)fs::remove_all(kTestGroupPath);
290298
config::l0_max_mem_usage = _old_l0_size;
291299
config::write_buffer_size = _old_memtable_size;
@@ -477,6 +485,100 @@ class LakePrimaryKeyConsistencyTest : public TestBase, testing::WithParamInterfa
477485
return Status::OK();
478486
}
479487

488+
// Build a delete chunk that reuses the first `count` keys of `src` (an upsert chunk), so a delete
489+
// targets keys that were just upserted in the same transaction.
490+
std::pair<ChunkPtr, std::vector<uint32_t>> gen_delete_from(const ChunkPtr& src, size_t count) {
491+
count = std::min<size_t>(count, static_cast<size_t>(src->num_rows()));
492+
std::vector<std::string> key_strs;
493+
std::vector<Slice> key_col;
494+
std::vector<int> c1v(count);
495+
std::vector<int> c2v(count);
496+
std::vector<uint8_t> ops(count, TOpType::DELETE);
497+
key_strs.reserve(count);
498+
for (size_t i = 0; i < count; i++) {
499+
key_strs.emplace_back(src->columns()[0]->get(i).get_slice().to_string());
500+
c1v[i] = src->columns()[1]->get(i).get_int32();
501+
c2v[i] = src->columns()[2]->get(i).get_int32();
502+
}
503+
for (const auto& s : key_strs) {
504+
key_col.emplace_back(s);
505+
}
506+
auto c0 = BinaryColumn::create();
507+
auto c1 = Int32Column::create();
508+
auto c2 = Int32Column::create();
509+
auto c3 = Int8Column::create();
510+
c0->append_strings(key_col.data(), key_col.size());
511+
c1->append_numbers(c1v.data(), c1v.size() * sizeof(int));
512+
c2->append_numbers(c2v.data(), c2v.size() * sizeof(int));
513+
c3->append_numbers(ops.data(), ops.size() * sizeof(uint8_t));
514+
auto indexes = std::vector<uint32_t>(count);
515+
for (uint32_t i = 0; i < count; i++) {
516+
indexes[i] = i;
517+
}
518+
return {std::make_shared<Chunk>(Columns{std::move(c0), std::move(c1), std::move(c2), std::move(c3)},
519+
_slot_cid_map),
520+
std::move(indexes)};
521+
}
522+
523+
// Apply a random INTERLEAVED sequence of upserts and deletes over a small pool of key sets within one
524+
// transaction, so a given key may be upserted, deleted, and re-upserted in any order. This exercises
525+
// in-transaction order preservation in both directions: a delete followed by a re-upsert of the same
526+
// key must keep the row, and an upsert followed by a delete must drop it. With
527+
// lake_enable_pk_preserve_txn_delete_order on (set in SetUp) and load spill on by default, this runs
528+
// through the op-aware spill merge. The Replayer replays the ops in write order, so any path that
529+
// resolves them out of order is caught.
530+
Status mixed_upsert_delete_op() {
531+
std::unique_ptr<ConfigResetGuard<int64_t>> force_index_mem_flush_guard = random_force_index_mem_flush();
532+
auto txn_id = next_id();
533+
ASSIGN_OR_ABORT(auto delta_writer, DeltaWriterBuilder()
534+
.set_tablet_manager(_tablet_mgr.get())
535+
.set_tablet_id(_tablet_metadata->id())
536+
.set_txn_id(txn_id)
537+
.set_partition_id(_partition_id)
538+
.set_mem_tracker(_mem_tracker.get())
539+
.set_schema_id(_tablet_schema->id())
540+
.set_slot_descriptors(&_slot_pointers)
541+
.set_profile(&_dummy_runtime_profile)
542+
.build());
543+
RETURN_IF_ERROR(delta_writer->open());
544+
// A pool of fresh key sets to interleave over; reusing them forces upserts and deletes to hit the
545+
// same keys, so their in-transaction order actually matters.
546+
size_t pool_size = std::max<size_t>(static_cast<size_t>(_random_generator->random() % MaxUpsert), 1);
547+
std::vector<ChunkPtr> key_pool;
548+
for (size_t i = 0; i < pool_size; i++) {
549+
key_pool.emplace_back(gen_upsert_data(true).first);
550+
}
551+
// At least 2 steps so an interleaving actually occurs.
552+
size_t steps = std::max<size_t>(static_cast<size_t>(_random_generator->random() % (MaxUpsert * 2)), 2);
553+
for (size_t s = 0; s < steps; s++) {
554+
const auto& base = key_pool[_random_generator->random() % key_pool.size()];
555+
if (base->num_rows() == 0) {
556+
continue;
557+
}
558+
if ((_random_generator->random() % 2) == 0) {
559+
// Upsert the pool chunk's keys.
560+
std::vector<uint32_t> indexes(base->num_rows());
561+
for (uint32_t i = 0; i < base->num_rows(); i++) {
562+
indexes[i] = i;
563+
}
564+
RETURN_IF_ERROR(delta_writer->write(*base, indexes.data(), indexes.size()));
565+
_replayer->upsert(base);
566+
} else {
567+
// Delete the pool chunk's keys.
568+
auto chunk_index = gen_delete_from(base, base->num_rows());
569+
RETURN_IF_ERROR(delta_writer->write(*(chunk_index.first), chunk_index.second.data(),
570+
chunk_index.second.size()));
571+
_replayer->erase(chunk_index.first);
572+
}
573+
}
574+
RETURN_IF_ERROR(delta_writer->finish_with_txnlog());
575+
delta_writer->close();
576+
// Publish version
577+
RETURN_IF_ERROR(publish_single_version(_tablet_metadata->id(), _version + 1, txn_id));
578+
_version++;
579+
return Status::OK();
580+
}
581+
480582
Status partial_update_op(PartialUpdateMode mode) {
481583
std::unique_ptr<ConfigResetGuard<int64_t>> force_index_mem_flush_guard = random_force_index_mem_flush();
482584
std::unique_ptr<ConfigResetGuard<bool>> pk_index_eager_build_guard = random_pk_index_eager_build();
@@ -679,6 +781,9 @@ class LakePrimaryKeyConsistencyTest : public TestBase, testing::WithParamInterfa
679781
case CONDITION_UPDATE:
680782
st = condition_update();
681783
break;
784+
case MIXED_UPSERT_DELETE:
785+
st = mixed_upsert_delete_op();
786+
break;
682787
default:
683788
break;
684789
}
@@ -731,6 +836,7 @@ class LakePrimaryKeyConsistencyTest : public TestBase, testing::WithParamInterfa
731836
int64_t _old_pk_index_eager_build_threshold_bytes = 0;
732837
int64_t _old_pk_index_parallel_execution_min_rows = 0;
733838
int64_t _old_pk_index_parallel_compaction_task_split_threshold_bytes = 0;
839+
bool _old_lake_enable_pk_preserve_txn_delete_order = false;
734840
};
735841

736842
TEST_P(LakePrimaryKeyConsistencyTest, test_local_pk_consistency) {

0 commit comments

Comments
 (0)