Skip to content

Commit 07f09fc

Browse files
committed
fix
1 parent 3cc4a84 commit 07f09fc

6 files changed

Lines changed: 160 additions & 6 deletions

File tree

‎include/paimon/reader/prefetch_file_batch_reader.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ class PAIMON_EXPORT PrefetchFileBatchReader : public FileBatchReader {
148148
/// Installs the sink above, or clears it when `sink` is empty. By default a reader has no
149149
/// late byte ranges to report and ignores the sink.
150150
/// @param sink The sink to report late byte ranges to.
151-
virtual void SetPreBufferSink(PreBufferSink /*sink*/) {}
151+
virtual void SetPreBufferSink(PreBufferSink sink) {}
152152
};
153153

154154
} // namespace paimon

‎include/paimon/utils/prefetch_cache_config.h‎

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class PAIMON_EXPORT CacheConfig {
5959
}
6060

6161
/// Returns the maximum gap size (in bytes) considered mergeable between
62-
/// adjacent ranges. Defaults to 8 KiB.
62+
/// adjacent ranges. Defaults to 512 KiB.
6363
uint64_t GetHoleSizeLimit() const {
6464
return hole_size_limit_;
6565
}
@@ -103,6 +103,31 @@ class PAIMON_EXPORT CacheConfig {
103103
block_cache_limit_ = block_cache_limit;
104104
}
105105

106+
/// Returns the alignment (in bytes) the adaptive range size is rounded up to, which is also
107+
/// the smallest size a range is cut to. Defaults to 4 MiB.
108+
uint64_t GetRangeSplitAlignment() const {
109+
return range_split_alignment_;
110+
}
111+
112+
/// Sets the alignment the adaptive range size is rounded up to. Zero turns the adaptive
113+
/// sizing off, so the ranges are cut at the configured size limit instead.
114+
void SetRangeSplitAlignment(uint64_t range_split_alignment) {
115+
range_split_alignment_ = range_split_alignment;
116+
}
117+
118+
/// Returns the number of requests the adaptive range size aims to spread one round of
119+
/// registered ranges over. Defaults to 10.
120+
uint64_t GetRangeSplitConcurrency() const {
121+
return range_split_concurrency_;
122+
}
123+
124+
/// Sets the number of requests the adaptive range size aims to spread one round of ranges
125+
/// over. Zero turns the adaptive sizing off, so the ranges are cut at the configured size
126+
/// limit instead.
127+
void SetRangeSplitConcurrency(uint64_t range_split_concurrency) {
128+
range_split_concurrency_ = range_split_concurrency;
129+
}
130+
106131
private:
107132
// The defaults are aligned with the reader's request granularity and with
108133
// realistic data file sizes:
@@ -117,11 +142,29 @@ class PAIMON_EXPORT CacheConfig {
117142
// at Init: those are fetched only just before they are read, and one range
118143
// is one request, so they are cut smaller than range_size_limit to be
119144
// fetched concurrently rather than in one long request. A read spanning
120-
// several of them is still served, as they are adjacent.
145+
// several of them is still served, as they are adjacent. It is an upper
146+
// bound only: the size such a round is actually cut at is derived from the
147+
// bytes it registers, see range_split_alignment below.
148+
// - hole_size_limit trades bytes against requests: coalescing across a gap
149+
// reads the gap too, but saves a request, and on remote storage a request
150+
// costs a round trip whatever its size. The limit is therefore well above
151+
// the page-sized gaps a filtered read leaves between the pages it keeps,
152+
// which would otherwise each cost a request of their own.
121153
uint64_t range_size_limit_ = 32 * 1024 * 1024;
122154
uint64_t late_range_size_limit_ = 8 * 1024 * 1024;
123-
uint64_t hole_size_limit_ = 8 * 1024;
155+
uint64_t hole_size_limit_ = 512 * 1024;
124156
uint64_t pre_buffer_limit_ = 256 * 1024 * 1024;
157+
// A fixed size limit cuts a small round into fewer requests than could be
158+
// fetched at once, leaving the storage idle while each of them runs. So the
159+
// size a round is cut at is derived from the round instead: its bytes are
160+
// spread over range_split_concurrency requests, rounded up to
161+
// range_split_alignment, and kept within the size limit above. A round
162+
// smaller than alignment * concurrency is therefore cut finer than that
163+
// limit and fetched in a single wave, while a larger one still stops at it.
164+
// The alignment is also the floor, so the derivation never cuts a round into
165+
// requests too small to amortize their own round trip.
166+
uint64_t range_split_alignment_ = 4 * 1024 * 1024;
167+
uint64_t range_split_concurrency_ = 10;
125168
// Blocks are aligned to the END of the file, so a block never reaches past
126169
// EOF. 64 KiB is the granularity the reads no prefetched range covers are
127170
// shared at: small enough that a metadata read at the tail of a file is

‎src/paimon/common/utils/byte_range_combiner.cpp‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,37 @@
2929

3030
namespace paimon {
3131

32+
namespace {
33+
34+
/// The size to cut `total_bytes` of ranges at so that they are spread over
35+
/// `target_concurrency` requests, rounded up to `alignment` and kept in
36+
/// (hole_size_limit, range_size_limit].
37+
uint64_t AdaptiveRangeSizeLimit(uint64_t total_bytes, uint64_t target_concurrency,
38+
uint64_t alignment, uint64_t hole_size_limit,
39+
uint64_t range_size_limit) {
40+
// Leave the limit alone when the derivation is off, and when it is not larger than the hole
41+
// size: CoalesceByteRanges() rejects such a pair, and it must report that as it always did
42+
// rather than have a derived limit paper over it.
43+
if (alignment == 0 || target_concurrency == 0 || range_size_limit <= hole_size_limit) {
44+
return range_size_limit;
45+
}
46+
const uint64_t per_request =
47+
total_bytes / target_concurrency + (total_bytes % target_concurrency == 0 ? 0 : 1);
48+
// One alignment unit is the floor, and the limit must stay above the hole size for the
49+
// reason above. hole_size_limit + 1 cannot overflow: it is below range_size_limit here.
50+
uint64_t limit = std::max({alignment, per_request, hole_size_limit + 1});
51+
if (const uint64_t remainder = limit % alignment; remainder != 0) {
52+
const uint64_t padding = alignment - remainder;
53+
if (limit > std::numeric_limits<uint64_t>::max() - padding) {
54+
return range_size_limit;
55+
}
56+
limit += padding;
57+
}
58+
return std::min(limit, range_size_limit);
59+
}
60+
61+
} // namespace
62+
3263
Result<std::vector<ByteRange>> ByteRangeCombiner::CoalesceByteRanges(
3364
std::vector<ByteRange>&& ranges, uint64_t hole_size_limit, uint64_t range_size_limit) {
3465
if (range_size_limit <= hole_size_limit) {
@@ -137,4 +168,20 @@ Result<std::vector<ByteRange>> ByteRangeCombiner::CoalesceByteRanges(
137168
return coalesced;
138169
}
139170

171+
Result<std::vector<ByteRange>> ByteRangeCombiner::CoalesceByteRangesAdaptive(
172+
std::vector<ByteRange>&& ranges, uint64_t hole_size_limit, uint64_t range_size_limit,
173+
uint64_t alignment, uint64_t target_concurrency) {
174+
uint64_t total_bytes = 0;
175+
for (const auto& range : ranges) {
176+
// Saturate rather than wrap. CoalesceByteRanges() rejects the ranges that can add up
177+
// this far, but only after the limit below has been derived from them.
178+
total_bytes = range.length > std::numeric_limits<uint64_t>::max() - total_bytes
179+
? std::numeric_limits<uint64_t>::max()
180+
: total_bytes + range.length;
181+
}
182+
return CoalesceByteRanges(std::move(ranges), hole_size_limit,
183+
AdaptiveRangeSizeLimit(total_bytes, target_concurrency, alignment,
184+
hole_size_limit, range_size_limit));
185+
}
186+
140187
} // namespace paimon

‎src/paimon/common/utils/byte_range_combiner.h‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ struct ByteRangeCombiner {
3232
static Result<std::vector<ByteRange>> CoalesceByteRanges(std::vector<ByteRange>&& ranges,
3333
uint64_t hole_size_limit,
3434
uint64_t range_size_limit);
35+
36+
/// Coalesces `ranges` like CoalesceByteRanges() does, except that the size the ranges are
37+
/// cut at is derived from how many bytes they cover instead of being fixed: the bytes are
38+
/// spread over `target_concurrency` ranges, the result is rounded up to a multiple of
39+
/// `alignment`, and it is kept within `range_size_limit`. So a small batch of ranges is cut
40+
/// into as many ranges as can be fetched at once rather than into fewer, longer ones, while
41+
/// a batch large enough to reach `range_size_limit` is cut exactly as CoalesceByteRanges()
42+
/// would cut it.
43+
///
44+
/// A zero `alignment` or `target_concurrency` turns the derivation off, falling back to
45+
/// `range_size_limit`.
46+
static Result<std::vector<ByteRange>> CoalesceByteRangesAdaptive(
47+
std::vector<ByteRange>&& ranges, uint64_t hole_size_limit, uint64_t range_size_limit,
48+
uint64_t alignment, uint64_t target_concurrency);
3549
};
3650

3751
} // namespace paimon

‎src/paimon/common/utils/byte_range_combiner_test.cpp‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

‎src/paimon/common/utils/read_ahead_cache.cpp‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,9 @@ Result<std::optional<uint64_t>> ReadAheadCache::Impl::AddRanges(std::vector<Byte
191191
uint64_t expected_round) {
192192
PAIMON_ASSIGN_OR_RAISE(
193193
std::vector<ByteRange> new_ranges,
194-
ByteRangeCombiner::CoalesceByteRanges(std::move(ranges), config_.GetHoleSizeLimit(),
195-
config_.GetLateRangeSizeLimit()));
194+
ByteRangeCombiner::CoalesceByteRangesAdaptive(
195+
std::move(ranges), config_.GetHoleSizeLimit(), config_.GetLateRangeSizeLimit(),
196+
config_.GetRangeSplitAlignment(), config_.GetRangeSplitConcurrency()));
196197
for (const auto& new_range : new_ranges) {
197198
PAIMON_RETURN_NOT_OK(ValidateValueInRange<int64_t>(new_range.offset, "range offset"));
198199
PAIMON_RETURN_NOT_OK(ValidateValueInRange<int64_t>(new_range.length, "range length"));

0 commit comments

Comments
 (0)