@@ -99,4 +99,53 @@ TEST(ByteRangeCombinerTest, TestRejectsRangesBeyondInt64Bound) {
9999 check_invalid ({{100 , 10 }, {0 , std::numeric_limits<uint64_t >::max ()}});
100100}
101101
102+ // The adaptive variant derives the size the ranges are cut at from the bytes they cover, so a
103+ // batch too small to fill the concurrency on its own is cut finer than the fixed limit cuts it.
104+ TEST (ByteRangeCombinerTest, TestAdaptiveRangeSizeLimit) {
105+ auto check = [](std::vector<ByteRange> ranges, uint64_t alignment, uint64_t target_concurrency,
106+ std::vector<ByteRange> expected) -> void {
107+ ASSERT_OK_AND_ASSIGN (auto coalesced,
108+ ByteRangeCombiner::CoalesceByteRangesAdaptive (
109+ std::move (ranges), /* hole_size_limit=*/ 1 ,
110+ /* range_size_limit=*/ 8 , alignment, target_concurrency));
111+ ASSERT_EQ (coalesced, expected);
112+ };
113+
114+ // 20 bytes over 10 requests is 2 per request, which the alignment lifts to one 4 byte
115+ // range each: cut at 4 instead of at the limit of 8.
116+ check ({{0 , 20 }}, /* alignment=*/ 4 , /* target_concurrency=*/ 10 ,
117+ {{0 , 4 }, {4 , 4 }, {8 , 4 }, {12 , 4 }, {16 , 4 }});
118+ // The same bytes cut at the fixed limit, for comparison: 3 ranges rather than 5.
119+ check ({{0 , 20 }}, /* alignment=*/ 0 , /* target_concurrency=*/ 10 , {{0 , 8 }, {8 , 8 }, {16 , 4 }});
120+ check ({{0 , 20 }}, /* alignment=*/ 4 , /* target_concurrency=*/ 0 , {{0 , 8 }, {8 , 8 }, {16 , 4 }});
121+ // 30 bytes over 3 requests is 10 each, which rounds up past the limit, so the limit wins
122+ // and the batch is cut exactly as the non-adaptive variant cuts it.
123+ check ({{0 , 30 }}, /* alignment=*/ 4 , /* target_concurrency=*/ 3 , {{0 , 8 }, {8 , 8 }, {16 , 8 }, {24 , 6 }});
124+ // The alignment is the floor: 10 bytes over 10 requests is 1 each, not cut into 1 byte
125+ // requests that could never amortize their own round trip.
126+ check ({{0 , 10 }}, /* alignment=*/ 4 , /* target_concurrency=*/ 10 , {{0 , 4 }, {4 , 4 }, {8 , 2 }});
127+ // A derived limit still coalesces across the holes that fit within it, and still bounds the
128+ // coalesced result: the third range would take it past 4 bytes, so it starts a new range.
129+ check ({{0 , 1 }, {2 , 1 }}, /* alignment=*/ 4 , /* target_concurrency=*/ 10 , {{0 , 3 }});
130+ check ({{0 , 1 }, {2 , 1 }, {4 , 1 }}, /* alignment=*/ 4 , /* target_concurrency=*/ 10 , {{0 , 3 }, {4 , 1 }});
131+ check ({}, /* alignment=*/ 4 , /* target_concurrency=*/ 10 , {});
132+ }
133+
134+ // CoalesceByteRanges() rejects a size limit that does not exceed the hole size, so the derived
135+ // limit must stay above it however small the batch is.
136+ TEST (ByteRangeCombinerTest, TestAdaptiveRangeSizeLimitStaysAboveHoleSize) {
137+ // 10 bytes over 10 requests would derive 4, below the hole size of 5.
138+ ASSERT_OK_AND_ASSIGN (auto coalesced, ByteRangeCombiner::CoalesceByteRangesAdaptive (
139+ {{0 , 10 }}, /* hole_size_limit=*/ 5 ,
140+ /* range_size_limit=*/ 8 , /* alignment=*/ 4 ,
141+ /* target_concurrency=*/ 10 ));
142+ ASSERT_EQ (coalesced, (std::vector<ByteRange>{{0 , 8 }, {8 , 2 }}));
143+
144+ // A limit that cannot exceed the hole size is still reported, not papered over.
145+ ASSERT_NOK_WITH_MSG (ByteRangeCombiner::CoalesceByteRangesAdaptive (
146+ {{0 , 10 }}, /* hole_size_limit=*/ 8 , /* range_size_limit=*/ 8 ,
147+ /* alignment=*/ 4 , /* target_concurrency=*/ 10 ),
148+ " should be larger than hole size limit" );
149+ }
150+
102151} // namespace paimon::test
0 commit comments