This is an automated email from the ASF dual-hosted git repository.

marin-ma pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new 48b5fca64d [GLUTEN-12985][VL] Handle mid-page EOS in 
VeloxRssSortShuffleReaderDeserializer (#12986)
48b5fca64d is described below

commit 48b5fca64d72b863960bca8129d11cbfdfae03d8
Author: Lingfeng Zhang <[email protected]>
AuthorDate: Thu Sep 17 21:48:07 2026 +0800

    [GLUTEN-12985][VL] Handle mid-page EOS in 
VeloxRssSortShuffleReaderDeserializer (#12986)
    
    * [GLUTEN-12985][VL] Handle mid-page EOS in 
VeloxRssSortShuffleReaderDeserializer
    
    GlutenByteInputStream::readBytes() drives next(true) in a for(;;) loop.
    When a page header declares more bytes than the stream actually holds
    (truncated partition data), the reader hits EOS mid-page — but
    VeloxInputStream::next() ignores its throwIfPastEnd argument and silently
    returns on EOS, so the loop never exits.
    
    This PR impl throwIfPassEnd arg of 
VeloxRssSortShuffleReaderDeserializer::VeloxInputStream::next()
    
    * Raise read errors via GLUTEN_ASSIGN_OR_THROW
    
    ---------
    
    Co-authored-by: Yuan <[email protected]>
---
 cpp/velox/shuffle/VeloxShuffleReader.cc   |  17 ++-
 cpp/velox/tests/CMakeLists.txt            |   2 +
 cpp/velox/tests/VeloxShuffleReaderTest.cc | 211 ++++++++++++++++++++++++++++++
 3 files changed, 223 insertions(+), 7 deletions(-)

diff --git a/cpp/velox/shuffle/VeloxShuffleReader.cc 
b/cpp/velox/shuffle/VeloxShuffleReader.cc
index d10a0ca305..6c2e101b40 100644
--- a/cpp/velox/shuffle/VeloxShuffleReader.cc
+++ b/cpp/velox/shuffle/VeloxShuffleReader.cc
@@ -845,7 +845,7 @@ 
VeloxRssSortShuffleReaderDeserializer::VeloxInputStream::VeloxInputStream(
     std::shared_ptr<arrow::io::InputStream> input,
     facebook::velox::BufferPtr buffer)
     : in_(std::move(input)), buffer_(std::move(buffer)) {
-  next(true);
+  next(false);
 }
 
 bool VeloxRssSortShuffleReaderDeserializer::VeloxInputStream::hasNext() {
@@ -853,7 +853,7 @@ bool 
VeloxRssSortShuffleReaderDeserializer::VeloxInputStream::hasNext() {
     return false;
   }
   if (ranges()[0].position >= ranges()[0].size) {
-    next(true);
+    next(false);
     return offset_ != 0;
   }
   return true;
@@ -861,11 +861,14 @@ bool 
VeloxRssSortShuffleReaderDeserializer::VeloxInputStream::hasNext() {
 
 void VeloxRssSortShuffleReaderDeserializer::VeloxInputStream::next(bool 
throwIfPastEnd) {
   const uint32_t readBytes = buffer_->capacity();
-  offset_ = in_->Read(readBytes, buffer_->asMutable<char>()).ValueOr(0);
-  if (offset_ > 0) {
-    int32_t realBytes = offset_;
-    VELOX_CHECK_LT(0, realBytes, "Reading past end of file.");
-    setRange({buffer_->asMutable<uint8_t>(), realBytes, 0});
+  offset_ = 0;
+  GLUTEN_ASSIGN_OR_THROW(int64_t realBytes, in_->Read(readBytes, 
buffer_->asMutable<char>()));
+  if (realBytes > 0) {
+    offset_ = realBytes;
+    setRange({buffer_->asMutable<uint8_t>(), static_cast<int32_t>(realBytes), 
0});
+  } else if (throwIfPastEnd) {
+    VELOX_FAIL(
+        "Reading past end of 
VeloxRssSortShuffleReaderDeserializer::VeloxInputStream, real bytes = {}", 
realBytes);
   }
 }
 
diff --git a/cpp/velox/tests/CMakeLists.txt b/cpp/velox/tests/CMakeLists.txt
index 136884503a..b3b368703a 100644
--- a/cpp/velox/tests/CMakeLists.txt
+++ b/cpp/velox/tests/CMakeLists.txt
@@ -114,6 +114,8 @@ add_velox_test(velox_rss_sort_shuffle_writer_test SOURCES
 add_velox_test(velox_sort_shuffle_writer_test SOURCES
                VeloxSortShuffleWriterTest.cc)
 
+add_velox_test(velox_shuffle_reader_test SOURCES VeloxShuffleReaderTest.cc)
+
 # TODO: ORC is not well supported. add_velox_test(orc_test SOURCES OrcTest.cc)
 add_velox_test(
   velox_operators_test
diff --git a/cpp/velox/tests/VeloxShuffleReaderTest.cc 
b/cpp/velox/tests/VeloxShuffleReaderTest.cc
new file mode 100644
index 0000000000..7cfab92c78
--- /dev/null
+++ b/cpp/velox/tests/VeloxShuffleReaderTest.cc
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Regression tests for the rss_sort shuffle reader, driven through the
+// public VeloxRssSortShuffleReaderDeserializer API via controllable fake
+// arrow::io::InputStreams.
+//
+// Covers:
+// - graceful EOS on an empty stream (e.g. an empty Celeborn partition);
+// - EOS hit mid-page on a truncated compressed page;
+// - a buggy upstream whose Read() returns an error status;
+//
+
+#include <gtest/gtest.h>
+
+#include <arrow/buffer.h>
+#include <arrow/io/interfaces.h>
+#include <arrow/memory_pool.h>
+
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "compute/VeloxBackend.h"
+#include "config/GlutenConfig.h"
+#include "memory/VeloxMemoryManager.h"
+#include "shuffle/VeloxShuffleReader.h"
+#include "tests/utils/TestAllocationListener.h"
+#include "tests/utils/TestStreamReader.h"
+#include "velox/common/base/tests/GTestUtils.h"
+#include "velox/serializers/PrestoSerializer.h"
+#include "velox/type/Type.h"
+#include "velox/vector/tests/utils/VectorTestBase.h"
+
+using namespace facebook::velox;
+using namespace facebook::velox::test;
+
+namespace gluten {
+
+namespace {
+// A minimal arrow::io::InputStream backed by a fixed in-memory payload. Once
+// the payload is exhausted, Read returns 0 (EOS). With `errorRead`, Read
+// returns an IOError instead, modeling a buggy upstream that fails the read.
+//
+// To keep a possible reader-side infinite loop (readBytes -> next() -> EOS ->
+// silently return -> spin) from hanging the test until the CI timeout, Read
+// throws after kMaxConsecutiveEosReads consecutive EOS returns. Well-behaved
+// readers probe EOS only a couple of times, so the cap never trips for them.
+class FakeInputStream final : public arrow::io::InputStream {
+ public:
+  explicit FakeInputStream(std::vector<uint8_t> payload = {}, bool errorRead = 
false)
+      : payload_(std::move(payload)), errorRead_(errorRead) {}
+
+  arrow::Status Close() override {
+    closed_ = true;
+    return arrow::Status::OK();
+  }
+  arrow::Result<int64_t> Tell() const override {
+    return pos_;
+  }
+  bool closed() const override {
+    return closed_;
+  }
+
+  arrow::Result<int64_t> Read(int64_t nbytes, void* out) override {
+    if (errorRead_) {
+      return arrow::Status::IOError("fake upstream read failure");
+    }
+    int64_t toRead = std::min<int64_t>(nbytes, 
static_cast<int64_t>(payload_.size()) - pos_);
+    if (toRead > 0) {
+      std::memcpy(out, payload_.data() + pos_, toRead);
+      pos_ += toRead;
+      consecutiveEosReads_ = 0;
+    } else if (++consecutiveEosReads_ > kMaxConsecutiveEosReads) {
+      // Throw a plain C++ exception as a loop guard: the EOS contract itself
+      // must not be an error (a clean 0 return is legitimate), so this is the
+      // only way to cut a possible spin short.
+      throw std::runtime_error(
+          "possible infinite loop: Read() returned 0 for " + 
std::to_string(kMaxConsecutiveEosReads) +
+          " consecutive calls");
+    }
+    return toRead; // 0 == EOS when payload exhausted
+  }
+
+  arrow::Result<std::shared_ptr<arrow::Buffer>> Read(int64_t nbytes) override {
+    GLUTEN_ASSIGN_OR_THROW(auto buffer, arrow::AllocateResizableBuffer(nbytes, 
arrow::default_memory_pool()));
+    GLUTEN_ASSIGN_OR_THROW(int64_t bytesRead, Read(nbytes, 
buffer->mutable_data()));
+    GLUTEN_THROW_NOT_OK(buffer->Resize(bytesRead, false));
+    buffer->ZeroPadding();
+    return std::move(buffer);
+  }
+
+ private:
+  static constexpr int32_t kMaxConsecutiveEosReads = 100;
+
+  std::vector<uint8_t> payload_;
+  int64_t pos_{0};
+  bool errorRead_{false};
+  int32_t consecutiveEosReads_{0};
+  bool closed_{false};
+};
+
+// Append a little-endian POD value to `out` (Presto page header fields are
+// machine byte order / little-endian on x86).
+template <typename T>
+void appendLe(std::vector<uint8_t>& out, T value) {
+  T v = value;
+  const auto* p = reinterpret_cast<const uint8_t*>(&v);
+  out.insert(out.end(), p, p + sizeof(T));
+}
+
+// Build a truncated Presto compressed page: a valid 21-byte header declaring
+// compressedSize bytes of body, but only `bodyBytes` bytes follow. The
+// reader's compressed branch calls source->readBytes(buf, compressedSize);
+// when EOS is hit mid-drain, GlutenByteInputStream::readBytes loops to
+// next(true) which must VELOX_FAIL instead of spinning.
+//
+// Header layout (PrestoHeader.cpp): numRows:int32, pageCodecMarker:int8,
+// uncompressedSize:int32, compressedSize:int32, checksum:int64 == 21 bytes.
+// pageCodecMarker = kCompressedBitMask (1), no checksum bit -> actualCheckSum
+// stays 0 and matches header.checksum = 0 (PrestoSerializer.cpp:159).
+std::vector<uint8_t> buildTruncatedCompressedPage(int32_t compressedSize, 
int32_t bodyBytes) {
+  std::vector<uint8_t> out;
+  out.reserve(21 + bodyBytes);
+  appendLe<int32_t>(out, /*numRows=*/1);
+  appendLe<int8_t>(out, /*pageCodecMarker=*/1); // kCompressedBitMask, no 
checksum
+  appendLe<int32_t>(out, /*uncompressedSize=*/compressedSize + 64);
+  appendLe<int32_t>(out, compressedSize);
+  appendLe<int64_t>(out, /*checksum=*/0);
+  for (int i = 0; i < bodyBytes; ++i) {
+    out.push_back(static_cast<uint8_t>(i & 0xFF));
+  }
+  return out;
+}
+} // namespace
+
+class VeloxShuffleReaderTest : public ::testing::Test, public 
test::VectorTestBase {
+ protected:
+  static void SetUpTestCase() {
+    if (!isRegisteredNamedVectorSerde("Presto")) {
+      serializer::presto::PrestoVectorSerde::registerNamedVectorSerde();
+    }
+    auto listener = std::make_unique<TestAllocationListener>();
+    std::unordered_map<std::string, std::string> 
conf{{kMemoryReservationBlockSize, "1"}, {kDebugModeEnabled, "true"}};
+    VeloxBackend::create(std::move(listener), conf);
+  }
+
+  static void TearDownTestCase() {
+    VeloxBackend::get()->tearDown();
+  }
+
+  std::shared_ptr<VeloxRssSortShuffleReaderDeserializer> 
makeDeserializer(std::shared_ptr<arrow::io::InputStream> in) {
+    auto streamReader = std::make_shared<TestStreamReader>(std::move(in));
+    return std::make_shared<VeloxRssSortShuffleReaderDeserializer>(
+        streamReader,
+        getDefaultMemoryManager(),
+        ROW({"c0"}, {INTEGER()}),
+        /*batchSize=*/1024,
+        common::CompressionKind_NONE,
+        deserializeTime_);
+  }
+
+  int64_t deserializeTime_{0};
+};
+
+// Empty stream (e.g. an empty Celeborn partition): construction must NOT
+// throw; next() returns nullptr (graceful EOS).
+TEST_F(VeloxShuffleReaderTest, EmptyStreamGracefulEos) {
+  auto deserializer = makeDeserializer(std::make_shared<FakeInputStream>());
+  EXPECT_EQ(deserializer->next(), nullptr);
+}
+
+// Truncated compressed page: header reads fine and construction succeeds,
+// but next() drives PrestoVectorSerde::deserialize's compressed branch ->
+// readBytes(compressedSize) -> next(true) on EOS -> VELOX_FAIL. Pre-fix this
+// is the documented infinite loop; post-fix it throws. See the file header
+// note for how FakeInputStream cuts the pre-fix loop short.
+TEST_F(VeloxShuffleReaderTest, EosMidPageThrows) {
+  auto payload = buildTruncatedCompressedPage(/*compressedSize=*/1000, 
/*bodyBytes=*/8);
+  auto deserializer = 
makeDeserializer(std::make_shared<FakeInputStream>(std::move(payload)));
+
+  VELOX_ASSERT_THROW(
+      deserializer->next(), "Reading past end of 
VeloxRssSortShuffleReaderDeserializer::VeloxInputStream");
+}
+
+// A buggy upstream whose Read returns an error status. The reader must
+// propagate it instead of swallowing it.
+TEST_F(VeloxShuffleReaderTest, ErrorReadThrows) {
+  auto deserializer = 
makeDeserializer(std::make_shared<FakeInputStream>(std::vector<uint8_t>{}, 
/*errorRead=*/true));
+
+  EXPECT_THROW((void)deserializer->next(), GlutenException);
+}
+
+} // namespace gluten


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to