This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 900935edf39 branch-4.2: [fix](rle) Fix infinite loop when a repeated
run reaches 2^30 values #69100 (#69193)
900935edf39 is described below
commit 900935edf39cd8ea0675c21b54799db55733978f
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 16 23:40:55 2026 +0800
branch-4.2: [fix](rle) Fix infinite loop when a repeated run reaches 2^30
values #69100 (#69193)
Cherry-picked from #69100
Co-authored-by: Xin Liao <[email protected]>
Co-authored-by: Xin Liao <[email protected]>
---
be/src/util/bit_stream_utils.h | 4 ++-
be/src/util/bit_stream_utils.inline.h | 8 +++---
be/src/util/faststring.h | 5 +++-
be/src/util/rle_encoding.h | 36 +++++++++++++++++++-----
be/test/util/bit_stream_utils_test.cpp | 25 +++++++++++++++++
be/test/util/faststring_test.cpp | 24 ++++++++++++++++
be/test/util/rle_encoding_test.cpp | 50 ++++++++++++++++++++++++++++++++++
7 files changed, 139 insertions(+), 13 deletions(-)
diff --git a/be/src/util/bit_stream_utils.h b/be/src/util/bit_stream_utils.h
index dc194bf5f8c..2bc82689bd7 100644
--- a/be/src/util/bit_stream_utils.h
+++ b/be/src/util/bit_stream_utils.h
@@ -60,7 +60,9 @@ public:
// Write a Vlq encoded int to the buffer. The value is written byte
aligned.
// For more details on vlq: en.wikipedia.org/wiki/Variable-length_quantity
- void PutVlqInt(int32_t v);
+ // Takes an unsigned value on purpose: the encoding shifts `v` right until
it
+ // is exhausted, which only terminates for a logical shift.
+ void PutVlqInt(uint32_t v);
// Get the index to the next aligned byte and advance the underlying
buffer by num_bytes.
size_t GetByteIndexAndAdvance(int num_bytes) {
diff --git a/be/src/util/bit_stream_utils.inline.h
b/be/src/util/bit_stream_utils.inline.h
index dba339a1b52..da233efd76a 100644
--- a/be/src/util/bit_stream_utils.inline.h
+++ b/be/src/util/bit_stream_utils.inline.h
@@ -86,12 +86,12 @@ void BitWriter::PutAligned(T val, int num_bytes) {
memcpy(ptr, &val, num_bytes);
}
-inline void BitWriter::PutVlqInt(int32_t v) {
- while ((v & 0xFFFFFF80) != 0L) {
- PutAligned<uint8_t>((v & 0x7F) | 0x80, 1);
+inline void BitWriter::PutVlqInt(uint32_t v) {
+ while ((v & 0xFFFFFF80U) != 0U) {
+ PutAligned<uint8_t>(static_cast<uint8_t>((v & 0x7F) | 0x80), 1);
v >>= 7;
}
- PutAligned<uint8_t>(v & 0x7F, 1);
+ PutAligned<uint8_t>(static_cast<uint8_t>(v & 0x7F), 1);
}
inline BitReader::BitReader(const uint8_t* buffer, int buffer_len)
diff --git a/be/src/util/faststring.h b/be/src/util/faststring.h
index 738444553d4..4e2a4c5d74f 100644
--- a/be/src/util/faststring.h
+++ b/be/src/util/faststring.h
@@ -106,7 +106,10 @@ public:
if (newcapacity <= capacity_) [[likely]] {
return;
}
- GrowArray(newcapacity);
+ // Grow by at least 50% rather than to exactly `newcapacity`. Callers
such
+ // as BitWriter reserve a few bytes at a time, and reallocating on
every
+ // call makes appending n bytes cost O(n^2) in memcpy.
+ GrowToAtLeast(newcapacity);
}
// Append the given data to the string, resizing capacity as necessary.
diff --git a/be/src/util/rle_encoding.h b/be/src/util/rle_encoding.h
index 1685ab85f1e..3489fdfd586 100644
--- a/be/src/util/rle_encoding.h
+++ b/be/src/util/rle_encoding.h
@@ -18,6 +18,8 @@
#include <glog/logging.h>
+#include <algorithm>
+#include <cstdint>
#include <limits> // IWYU pragma: keep
#include "common/cast_set.h"
@@ -157,6 +159,16 @@ public:
Clear();
}
+ // The largest number of values a single repeated run can describe.
+ //
+ // A repeated run is written as a vlq-encoded indicator holding
+ // `run_length << 1` (see FlushRepeatedRun) and read back into a uint32_t
by
+ // BitReader::GetVlqInt. Capping the run length here keeps the indicator
+ // inside int32_t's positive range, which is exactly the range every
released
+ // version could already encode, so the output stays byte-identical for any
+ // run length that used to work.
+ static constexpr int64_t MAX_VALUES_PER_REPEATED_RUN = (int64_t {1} << 30)
- 1;
+
// Reserve 'num_bytes' bytes for a plain encoded header, set each
// byte with 'val': this is used for the RLE-encoded data blocks in
// order to be able to able to store the initial ordinal position
@@ -213,7 +225,7 @@ private:
// if we are in a literal run. If the repeat_count_ get high enough, we
switch
// to encoding repeated runs.
uint64_t current_value_;
- int repeat_count_;
+ int64_t repeat_count_;
// Number of literals in the current run. This does not include the
literals
// that might be in buffered_values_. Only after we've got a group big
enough
@@ -438,7 +450,7 @@ void RleEncoder<T>::Put(T value, size_t run_length) {
// Fast path: if this is a continuation of the current repeated run and
// we've already buffered enough values, just increment repeat_count_
if (current_value_ == value && repeat_count_ >= 8 && run_length > 0)
[[likely]] {
- repeat_count_ += run_length;
+ repeat_count_ += static_cast<int64_t>(run_length);
return;
}
@@ -458,7 +470,7 @@ void RleEncoder<T>::Put(T value, size_t run_length) {
// After flushing, if we still have a repeated run and more
values,
// we can add them directly to repeat_count_
if (repeat_count_ >= 8 && run_length > 0) {
- repeat_count_ += run_length;
+ repeat_count_ += static_cast<int64_t>(run_length);
return;
}
}
@@ -515,11 +527,21 @@ void RleEncoder<T>::FlushLiteralRun(bool
update_indicator_byte) {
template <typename T>
void RleEncoder<T>::FlushRepeatedRun() {
DCHECK_GT(repeat_count_, 0);
- // The lsb of 0 indicates this is a repeated run
- int32_t indicator_value = repeat_count_ << 1 | 0;
- bit_writer_.PutVlqInt(indicator_value);
- bit_writer_.PutAligned(current_value_, BitUtil::Ceil(bit_width_, 8));
+ // A run longer than MAX_VALUES_PER_REPEATED_RUN cannot be described by a
+ // single indicator, so emit it as several back-to-back repeated runs of
the
+ // same value. Decoders stitch those back together transparently
+ // (RleDecoder::GetNextRun merges adjacent runs carrying the same value),
so
+ // splitting is invisible to readers.
+ while (repeat_count_ > 0) {
+ const int64_t run_length = std::min(repeat_count_,
MAX_VALUES_PER_REPEATED_RUN);
+ // The lsb of 0 indicates this is a repeated run
+ bit_writer_.PutVlqInt(static_cast<uint32_t>(run_length) << 1);
+ bit_writer_.PutAligned(current_value_, BitUtil::Ceil(bit_width_, 8));
+ repeat_count_ -= run_length;
+ }
num_buffered_values_ = 0;
+ // The loop above drains repeat_count_; assign it anyway so the
postcondition
+ // holds even if it somehow entered non-positive.
repeat_count_ = 0;
}
diff --git a/be/test/util/bit_stream_utils_test.cpp
b/be/test/util/bit_stream_utils_test.cpp
index 3a078f004c0..55976ce4ee3 100644
--- a/be/test/util/bit_stream_utils_test.cpp
+++ b/be/test/util/bit_stream_utils_test.cpp
@@ -218,4 +218,29 @@ TEST(TestBitStreamUtil, TestUint64) {
reader.GetValue(16, &v4);
EXPECT_EQ(v4, 126);
}
+// PutVlqInt() must terminate for every uint32_t. It used to take an int32_t
and
+// shift it right arithmetically, so any value with the sign bit set (>= 2^31)
+// made the loop spin forever appending 0xFF.
+TEST(TestBitStreamUtil, TestPutVlqIntRoundTrip) {
+ const int max_vlq_byte_len = BitReader::MAX_VLQ_BYTE_LEN;
+ const std::vector<uint32_t> values = {
+ 0, 1, 0x7F, 0x80, 0x3FFF, 0x4000,
+ 0x1FFFFF, 0x200000, 0xFFFFFFF, 0x10000000, 0x7FFFFFFE,
0x7FFFFFFF,
+ 0x80000000, 0x80000001, 0xFFFFFFFE, 0xFFFFFFFF,
+ };
+
+ for (uint32_t value : values) {
+ faststring buffer;
+ BitWriter writer(&buffer);
+ writer.PutVlqInt(value);
+ writer.Flush();
+ EXPECT_LE(writer.bytes_written(), max_vlq_byte_len) << "value=" <<
value;
+
+ BitReader reader(buffer.data(), buffer.size());
+ uint32_t decoded = 0;
+ ASSERT_TRUE(reader.GetVlqInt(&decoded)) << "value=" << value;
+ EXPECT_EQ(value, decoded) << "value=" << value;
+ }
+}
+
} // namespace doris
diff --git a/be/test/util/faststring_test.cpp b/be/test/util/faststring_test.cpp
index 1abcd7e97fb..ea708337ab2 100644
--- a/be/test/util/faststring_test.cpp
+++ b/be/test/util/faststring_test.cpp
@@ -135,4 +135,28 @@ TEST_F(FaststringTest, TestAppend_ExponentiallyExpand) {
}
}
+// reserve() must grow geometrically. BitWriter reserves a handful of bytes at
a
+// time, so reallocating to the exact requested size made appending n bytes
cost
+// O(n^2) in memcpy -- enough to stall a thread outright once the buffer
reached
+// hundreds of megabytes.
+TEST_F(FaststringTest, TestReserve_ExponentiallyExpand) {
+ constexpr size_t kTargetSize = 1 * 1024 * 1024;
+
+ faststring s;
+ size_t last_capacity = s.capacity();
+ int reallocations = 0;
+ for (size_t i = 0; i < kTargetSize; ++i) {
+ s.reserve(i + 1);
+ if (s.capacity() != last_capacity) {
+ ++reallocations;
+ last_capacity = s.capacity();
+ }
+ }
+
+ EXPECT_GE(s.capacity(), kTargetSize);
+ // Growing 1.5x at a time from kInitialCapacity needs ~26 steps to pass 1
MiB.
+ // Exact growth would instead reallocate on (almost) every one of the 1M
calls.
+ EXPECT_LT(reallocations, 64);
+}
+
} // namespace doris
diff --git a/be/test/util/rle_encoding_test.cpp
b/be/test/util/rle_encoding_test.cpp
index 5af34168869..59b9368edce 100644
--- a/be/test/util/rle_encoding_test.cpp
+++ b/be/test/util/rle_encoding_test.cpp
@@ -698,6 +698,56 @@ TEST_F(TestRle, TestPutFlushThenFastPath) {
}
}
+// Regression test for a hang on repeated runs that a single run indicator
+// cannot describe.
+//
+// FlushRepeatedRun() used to write `repeat_count_ << 1` into an int32_t, so a
+// run of 2^30 or more values overflowed into a negative indicator.
+// BitWriter::PutVlqInt() then took an int32_t and shifted it right
+// arithmetically, which sticks at -1 instead of reaching 0 -- an infinite loop
+// appending 0xFF to the output buffer. In the wild this showed up as a base
+// compaction thread spinning forever while flushing the null bitmap of a
+// nullable column whose page held one run of 2^30 null rows.
+TEST_F(TestRle, TestRepeatedRunExceedingIndicatorRange) {
+ constexpr int64_t kMaxRun = RleEncoder<bool>::MAX_VALUES_PER_REPEATED_RUN;
+ const std::vector<int64_t> run_lengths = {
+ kMaxRun - 1,
+ kMaxRun,
+ kMaxRun + 1,
+ int64_t {1} << 30, // smallest run length that used to overflow
+ (int64_t {1} << 30) + 12345,
+ int64_t {1} << 31,
+ 3 * (int64_t {1} << 30),
+ };
+
+ for (int64_t run_length : run_lengths) {
+ faststring buffer;
+ RleEncoder<bool> encoder(&buffer, 1);
+ // Put() extends an established run in O(1), so this stays cheap even
for
+ // billions of values.
+ encoder.Put(true, run_length);
+ const int encoded_len = encoder.Flush();
+
+ // A split run stays compact: one short header per emitted run.
+ const int64_t expected_runs = (run_length + kMaxRun - 1) / kMaxRun;
+ EXPECT_LE(encoded_len, expected_runs * 8 + 8) << "run_length=" <<
run_length;
+
+ // GetNextRun() merges adjacent repeated runs carrying the same value,
so
+ // splitting is invisible to readers: this must read back as one run of
+ // the original length.
+ RleDecoder<bool> decoder(buffer.data(), encoded_len, 1);
+ int64_t decoded = 0;
+ while (decoded < run_length) {
+ bool val = false;
+ size_t run = decoder.GetNextRun(&val,
static_cast<size_t>(run_length - decoded));
+ ASSERT_GT(run, 0u) << "run_length=" << run_length << " decoded="
<< decoded;
+ ASSERT_TRUE(val) << "run_length=" << run_length << " decoded=" <<
decoded;
+ decoded += static_cast<int64_t>(run);
+ }
+ EXPECT_EQ(decoded, run_length) << "run_length=" << run_length;
+ }
+}
+
// Benchmark for Put function performance with consecutive equal values
// This test compares:
// 1. Put(value) called N times in a loop
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]