@@ -26,6 +26,8 @@ const object_id oid1 = l1::create_object_id();
2626const object_id oid2 = l1::create_object_id();
2727const object_id oid3 = l1::create_object_id();
2828const object_id oid4 = l1::create_object_id();
29+ const object_id oid5 = l1::create_object_id();
30+ const object_id oid6 = l1::create_object_id();
2931const std::string_view tidp_a = " deadbeef-aaaa-0000-0000-000000000000/0" ;
3032const std::string_view tidp_b = " deadbeef-bbbb-0000-0000-000000000000/0" ;
3133const std::string_view tidp_c = " deadbeef-cccc-0000-0000-000000000000/0" ;
@@ -498,6 +500,168 @@ TEST(StateUpdateTest, TestEmptyReplace) {
498500 testing::StrEq (" No objects requested" ));
499501}
500502
503+ TEST (StateUpdateTest, TestReplaceValidNonContiguous) {
504+ auto add = add_objects_builder ()
505+ .add (new_obj_builder (oid1, 100 , 1100 )
506+ .add (tidp_a, 0_o, 99_o, 1999_t , 0 , 99 )
507+ .build ())
508+ .add (new_obj_builder (oid2, 100 , 1100 )
509+ .add (tidp_a, 100_o, 199_o, 1999_t , 0 , 99 )
510+ .build ())
511+ .add (new_obj_builder (oid3, 100 , 1100 )
512+ .add (tidp_a, 200_o, 299_o, 1999_t , 0 , 99 )
513+ .build ())
514+ .add_term_start (tidp_a, 0_tm, 0_o)
515+ .build ();
516+ state s;
517+ auto add_res = add.apply (s);
518+ ASSERT_TRUE (add_res.has_value ());
519+
520+ // Attempt to replace oid1 and oid3 while leaving oid2 in place with a
521+ // non-contiguous update. While the update itself is non-contiguous, the
522+ // individual objects still align with existing extents, and is therefore
523+ // valid.
524+ auto replace = replace_objects_builder ()
525+ .add (new_obj_builder (oid4, 100 , 1100 )
526+ .add (tidp_a, 0_o, 99_o, 1999_t , 0 , 99 )
527+ .build ())
528+ .add (new_obj_builder (oid5, 100 , 1100 )
529+ .add (tidp_a, 200_o, 299_o, 1999_t , 0 , 99 )
530+ .build ())
531+ .build ();
532+
533+ auto replace_res = replace.apply (s);
534+ ASSERT_TRUE (replace_res.has_value ());
535+
536+ auto & p = s.partition_state (model::topic_id_partition::from (tidp_a))->get ();
537+ ASSERT_EQ (p.extents .size (), 3 );
538+ }
539+
540+ TEST (StateUpdateTest, TestReplaceValidNonContiguousSplitExtent) {
541+ auto add = add_objects_builder ()
542+ .add (new_obj_builder (oid1, 100 , 1100 )
543+ .add (tidp_a, 0_o, 99_o, 1999_t , 0 , 99 )
544+ .build ())
545+ .add (new_obj_builder (oid2, 100 , 1100 )
546+ .add (tidp_a, 100_o, 199_o, 1999_t , 0 , 99 )
547+ .build ())
548+ .add (new_obj_builder (oid3, 100 , 1100 )
549+ .add (tidp_a, 200_o, 299_o, 1999_t , 0 , 99 )
550+ .build ())
551+ .add_term_start (tidp_a, 0_tm, 0_o)
552+ .build ();
553+ state s;
554+ auto add_res = add.apply (s);
555+ ASSERT_TRUE (add_res.has_value ());
556+
557+ // Attempt to replace oid1 and oid3 while leaving oid2 in place with a
558+ // non-contiguous update whose objects align with existing extents.
559+ // The update should see oid3 split into two new extents (for a total of 4
560+ // extents).
561+ auto replace = replace_objects_builder ()
562+ .add (new_obj_builder (oid4, 100 , 1100 )
563+ .add (tidp_a, 0_o, 99_o, 1999_t , 0 , 99 )
564+ .build ())
565+ .add (new_obj_builder (oid5, 100 , 1100 )
566+ .add (tidp_a, 200_o, 249_o, 1999_t , 0 , 99 )
567+ .build ())
568+ .add (new_obj_builder (oid6, 100 , 1100 )
569+ .add (tidp_a, 250_o, 299_o, 1999_t , 0 , 99 )
570+ .build ())
571+ .build ();
572+
573+ auto replace_res = replace.apply (s);
574+ ASSERT_TRUE (replace_res.has_value ());
575+
576+ auto & p = s.partition_state (model::topic_id_partition::from (tidp_a))->get ();
577+ ASSERT_EQ (p.extents .size (), 4 );
578+ }
579+
580+ TEST (StateUpdateTest, TestReplaceInvalidNonContiguousBadOffsets) {
581+ using testing::ElementsAre;
582+ auto add = add_objects_builder ()
583+ .add (new_obj_builder (oid1, 100 , 1100 )
584+ .add (tidp_a, 0_o, 99_o, 1999_t , 0 , 99 )
585+ .build ())
586+ .add (new_obj_builder (oid2, 100 , 1100 )
587+ .add (tidp_a, 100_o, 199_o, 1999_t , 0 , 99 )
588+ .build ())
589+ .add (new_obj_builder (oid3, 100 , 1100 )
590+ .add (tidp_a, 200_o, 299_o, 1999_t , 0 , 99 )
591+ .build ())
592+ .add_term_start (tidp_a, 0_tm, 0_o)
593+ .build ();
594+ state s;
595+ auto add_res = add.apply (s);
596+ ASSERT_TRUE (add_res.has_value ());
597+
598+ // Attempt to replace oid1 and oid3 while leaving oid2 in place with a
599+ // invalid non-contiguous update with bad offsets.
600+ auto replace = replace_objects_builder ()
601+ .add (new_obj_builder (oid4, 100 , 1100 )
602+ .add (tidp_a, 0_o, 99_o, 1999_t , 0 , 99 )
603+ .build ())
604+ .add (new_obj_builder (oid5, 100 , 1100 )
605+ .add (tidp_a, 200_o, 249_o, 1999_t , 0 , 99 )
606+ .build ())
607+ .add (new_obj_builder (oid6, 100 , 1100 )
608+ .add (tidp_a, 239_o, 299_o, 1999_t , 0 , 99 )
609+ .build ())
610+ .build ();
611+
612+ auto replace_res = replace.apply (s);
613+ ASSERT_FALSE (replace_res.has_value ());
614+ EXPECT_THAT (
615+ std::string (replace_res.error ()()),
616+ testing::ContainsRegex (" breaks partition .+ offset ordering" ));
617+
618+ auto & p = s.partition_state (model::topic_id_partition::from (tidp_a))->get ();
619+ ASSERT_EQ (p.extents .size (), 3 );
620+ }
621+
622+ TEST (StateUpdateTest, TestReplaceInvalidNonContiguousDoesNotSpan) {
623+ using testing::ElementsAre;
624+ auto add = add_objects_builder ()
625+ .add (new_obj_builder (oid1, 100 , 1100 )
626+ .add (tidp_a, 0_o, 99_o, 1999_t , 0 , 99 )
627+ .build ())
628+ .add (new_obj_builder (oid2, 100 , 1100 )
629+ .add (tidp_a, 100_o, 199_o, 1999_t , 0 , 99 )
630+ .build ())
631+ .add (new_obj_builder (oid3, 100 , 1100 )
632+ .add (tidp_a, 200_o, 299_o, 1999_t , 0 , 99 )
633+ .build ())
634+ .add_term_start (tidp_a, 0_tm, 0_o)
635+ .build ();
636+ state s;
637+ auto add_res = add.apply (s);
638+ ASSERT_TRUE (add_res.has_value ());
639+
640+ // Attempt to replace oid1 and oid3 while leaving oid2 in place with a
641+ // invalid non-contiguous update that doesn't exactly span existing extents.
642+ auto replace = replace_objects_builder ()
643+ .add (new_obj_builder (oid4, 100 , 1100 )
644+ .add (tidp_a, 0_o, 99_o, 1999_t , 0 , 99 )
645+ .build ())
646+ .add (new_obj_builder (oid5, 100 , 1100 )
647+ .add (tidp_a, 200_o, 249_o, 1999_t , 0 , 99 )
648+ .build ())
649+ .add (new_obj_builder (oid6, 100 , 1100 )
650+ .add (tidp_a, 250_o, 298_o, 1999_t , 0 , 99 )
651+ .build ())
652+ .build ();
653+
654+ auto replace_res = replace.apply (s);
655+ ASSERT_FALSE (replace_res.has_value ());
656+ EXPECT_THAT (
657+ std::string (replace_res.error ()()),
658+ testing::ContainsRegex (
659+ " Partition .+ doesn't contain extents that span exactly" ));
660+
661+ auto & p = s.partition_state (model::topic_id_partition::from (tidp_a))->get ();
662+ ASSERT_EQ (p.extents .size (), 3 );
663+ }
664+
501665TEST (StateUpdateTest, TestReplaceWithCompaction) {
502666 using testing::ElementsAre;
503667 using range = struct compaction_state_update ::cleaned_range;
0 commit comments