Skip to content

Commit 17bb771

Browse files
Merge pull request #29999 from vbotbuildovich/backport-pr-29995-v26.1.x-573
[v26.1.x] ct/l1: fix prefix truncation race in partition_validator
2 parents da3d473 + dc345b7 commit 17bb771

2 files changed

Lines changed: 79 additions & 0 deletions

File tree

src/v/cloud_topics/level_one/metastore/partition_validator.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ partition_validator::validate(validate_partition_options opts) {
195195
co_return result;
196196
}
197197
const auto& metadata = metadata_res.value().value();
198+
if (
199+
opts.resume_at_offset.has_value()
200+
&& *opts.resume_at_offset < metadata.start_offset) {
201+
// A prefix truncation may have advanced start_offset past the
202+
// resume point between paginated calls. Clamp to start_offset
203+
// so pagination skips past the truncated region.
204+
opts.resume_at_offset = metadata.start_offset;
205+
}
198206

199207
auto seen_objects_res = co_await validate_extents(opts, metadata, result);
200208
if (!seen_objects_res.has_value()) {

src/v/cloud_topics/level_one/metastore/tests/partition_validator_test.cc

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,74 @@ TEST_F(PartitionValidatorTest, CompactionValidAfterSetStartOffset) {
401401
write_compaction(tp_a, std::move(c));
402402
expect_clean(validate({.tidp = tp_a}));
403403
}
404+
405+
// Simulate a prefix truncation between paginated validation calls. Page 1
406+
// validates [0,99] and returns resume_at_offset=0. Before page 2,
407+
// set_start_offset advances start to 100 and removes the [0,99] extent. The
408+
// validator should detect that resume_at is below start_offset and fall back
409+
// to first-page behavior.
410+
TEST_F(PartitionValidatorTest, PrefixTruncationDuringPagination) {
411+
auto o1 = create_object_id();
412+
auto o2 = create_object_id();
413+
add_extent(tp_a, o1, 0_o, 99_o);
414+
add_extent(tp_a, o2, 100_o, 199_o);
415+
416+
auto p1 = validate({.tidp = tp_a, .max_extents = 1});
417+
expect_clean(p1);
418+
ASSERT_TRUE(p1.resume_at_offset.has_value());
419+
420+
// Simulate prefix truncation: advance start_offset, remove first extent.
421+
write_metadata(tp_a, 100_o, 200_o);
422+
{
423+
auto wb = db_->create_write_batch();
424+
wb.remove(extent_row_key::encode(tp_a, 0_o), next_seqno());
425+
db_->apply(std::move(wb)).get();
426+
}
427+
428+
// Page 2 with stale resume_at_offset: clamped to start_offset=100,
429+
// re-reads [100,199] (exact match, not counted), no more extents.
430+
auto p2 = validate({.tidp = tp_a, .resume_at_offset = p1.resume_at_offset});
431+
expect_clean(p2);
432+
EXPECT_FALSE(p2.resume_at_offset.has_value());
433+
}
434+
435+
TEST_F(PartitionValidatorTest, MidExtentStartWithPagination) {
436+
auto o1 = create_object_id();
437+
auto o2 = create_object_id();
438+
write_metadata(tp_a, 50_o, 200_o);
439+
440+
// NOTE: first extent falls below start offset.
441+
write_extent(tp_a, 0_o, 99_o, o1);
442+
write_extent(tp_a, 100_o, 199_o, o2);
443+
write_object(o1);
444+
write_object(o2);
445+
write_term(tp_a, model::term_id{1}, 0_o);
446+
447+
auto p1 = validate({.tidp = tp_a, .max_extents = 1});
448+
expect_clean(p1);
449+
EXPECT_EQ(p1.extents_validated, 1);
450+
ASSERT_TRUE(p1.resume_at_offset.has_value());
451+
452+
// The returned resume should be the first extent, which is below the start
453+
// offset.
454+
EXPECT_EQ(*p1.resume_at_offset, 0_o);
455+
456+
// We should be able to page through the rest of the partition without
457+
// issues (e.g. no looping on the same extent).
458+
auto p2 = validate(
459+
{.tidp = tp_a,
460+
.resume_at_offset = p1.resume_at_offset,
461+
.max_extents = 1});
462+
expect_clean(p2);
463+
EXPECT_TRUE(p2.resume_at_offset.has_value());
464+
EXPECT_EQ(*p2.resume_at_offset, 100_o);
465+
EXPECT_GE(p2.extents_validated, 1);
466+
467+
auto p3 = validate(
468+
{.tidp = tp_a,
469+
.resume_at_offset = p2.resume_at_offset,
470+
.max_extents = 1});
471+
expect_clean(p3);
472+
EXPECT_FALSE(p3.resume_at_offset.has_value());
473+
EXPECT_GE(p3.extents_validated, 0);
474+
}

0 commit comments

Comments
 (0)