This is an automated email from the ASF dual-hosted git repository.
RexXiong pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git
The following commit(s) were added to refs/heads/main by this push:
new 38f823141c [CELEBORN-2355] Return an empty stream from C++
ShuffleClientImpl::readPartition for an empty partition
38f823141c is described below
commit 38f823141cbb794d754185d9d24623535fd63302
Author: Nicholas Jiang <[email protected]>
AuthorDate: Tue Jun 30 10:15:03 2026 +0800
[CELEBORN-2355] Return an empty stream from C++
ShuffleClientImpl::readPartition for an empty partition
### What changes were proposed in this pull request?
The C++ `ShuffleClientImpl::readPartition` now returns an empty stream when
a shuffle has no locations for the requested partition, instead of constructing
a `CelebornInputStream` over zero locations. Specifically:
- Add a `CelebornInputStream::empty()` factory that builds a no-op stream
whose `read()` immediately yields `-1`.
- In `readPartition`, when `locations` is empty, log a warning and return
`CelebornInputStream::empty()`; the shuffle key is now built only on the
non-empty path.
Tests:
- A unit test for `CelebornInputStream::empty()` (immediate EOF, repeated
reads, zero-length read).
- Two `readPartition` tests (empty file groups, missing partition) backed
by a `VisibleForTesting` seam that seeds the cached file-group response so no
RPC to the `LifecycleManager` is needed.
### Why are the changes needed?
For a partition with no shuffle data, the previous code constructed a full
reader over an empty location list (also building the decompressor when
compression is enabled). Returning an empty stream short-circuits this and
makes the empty-shuffle case explicit, consistent with the read path's intent.
### Does this PR resolve a correctness bug?
- [ ] Yes
### Does this PR introduce _any_ user-facing change?
- [ ] Yes
### How was this patch tested?
Added unit tests under `cpp/celeborn/client/tests/`:
- `CelebornInputStreamTest.emptyStreamReadsReturnEof`
- `ShuffleClientImplTest.readPartitionEmptyWhenFileGroupsEmpty`
- `ShuffleClientImplTest.readPartitionEmptyWhenPartitionMissing`
Run with:
```
cd cpp && mkdir -p build && cd build && cmake .. && make
celeborn_client_test
./celeborn/client/tests/celeborn_client_test \
--gtest_filter='CelebornInputStreamTest.*:ShuffleClientImplTest.readPartition*'
```
Closes #3748 from SteNicholas/CELEBORN-2355.
Authored-by: Nicholas Jiang <[email protected]>
Signed-off-by: Shuang <[email protected]>
---
cpp/celeborn/client/ShuffleClient.cpp | 8 +++-
cpp/celeborn/client/ShuffleClient.h | 8 ++++
cpp/celeborn/client/reader/CelebornInputStream.cpp | 20 ++++++++
cpp/celeborn/client/reader/CelebornInputStream.h | 7 +++
cpp/celeborn/client/tests/CMakeLists.txt | 1 +
.../client/tests/CelebornInputStreamTest.cpp | 40 ++++++++++++++++
.../client/tests/ShuffleClientImplTest.cpp | 54 ++++++++++++++++++++++
7 files changed, 137 insertions(+), 1 deletion(-)
diff --git a/cpp/celeborn/client/ShuffleClient.cpp
b/cpp/celeborn/client/ShuffleClient.cpp
index 2549e5f992..45f6798a9d 100644
--- a/cpp/celeborn/client/ShuffleClient.cpp
+++ b/cpp/celeborn/client/ShuffleClient.cpp
@@ -818,13 +818,19 @@ std::unique_ptr<CelebornInputStream>
ShuffleClientImpl::readPartition(
bool needCompression) {
const auto reducerFileGroupInfo = getReducerFileGroupInfo(shuffleId);
CELEBORN_CHECK_NOT_NULL(reducerFileGroupInfo);
- std::string shuffleKey = utils::makeShuffleKey(appUniqueId_, shuffleId);
std::vector<std::shared_ptr<const protocol::PartitionLocation>> locations;
if (!reducerFileGroupInfo->fileGroups.empty() &&
reducerFileGroupInfo->fileGroups.count(partitionId)) {
locations = std::move(utils::toVector(
reducerFileGroupInfo->fileGroups.find(partitionId)->second));
}
+ // No locations: return an empty stream instead of building a reader.
+ if (locations.empty()) {
+ LOG(WARNING) << "Shuffle data is empty for shuffle " << shuffleId
+ << " partition " << partitionId << ".";
+ return CelebornInputStream::empty();
+ }
+ std::string shuffleKey = utils::makeShuffleKey(appUniqueId_, shuffleId);
return std::make_unique<CelebornInputStream>(
shuffleKey,
conf_,
diff --git a/cpp/celeborn/client/ShuffleClient.h
b/cpp/celeborn/client/ShuffleClient.h
index e2db120dc8..ee06dec0ba 100644
--- a/cpp/celeborn/client/ShuffleClient.h
+++ b/cpp/celeborn/client/ShuffleClient.h
@@ -289,6 +289,14 @@ class ShuffleClientImpl
std::optional<protocol::StatusCode> getPushTargetWorkerExcludeCause(
const protocol::PartitionLocation& location);
+ // @VisibleForTesting. Seeds the cached file-group response so readPartition
+ // runs without an RPC: getReducerFileGroupInfo then hits the cache.
+ void setReducerFileGroupInfoForTest(
+ int shuffleId,
+ std::shared_ptr<protocol::GetReducerFileGroupResponse> info) {
+ reducerFileGroupInfos_.set(shuffleId, std::move(info));
+ }
+
private:
std::shared_ptr<PushState> getPushState(const std::string& mapKey);
diff --git a/cpp/celeborn/client/reader/CelebornInputStream.cpp
b/cpp/celeborn/client/reader/CelebornInputStream.cpp
index f81ca0d1a9..6788cbcb08 100644
--- a/cpp/celeborn/client/reader/CelebornInputStream.cpp
+++ b/cpp/celeborn/client/reader/CelebornInputStream.cpp
@@ -77,6 +77,26 @@ CelebornInputStream::CelebornInputStream(
moveToNextReader();
}
+CelebornInputStream::CelebornInputStream()
+ : attemptNumber_(0),
+ startMapIndex_(0),
+ endMapIndex_(0),
+ shouldDecompress_(false),
+ currLocationIndex_(0),
+ currBatchPos_(0),
+ currBatchSize_(0),
+ fetchChunkRetryCnt_(0),
+ fetchChunkMaxRetry_(0),
+ retryWait_(utils::MS::zero()),
+ fetchExcludedWorkerExpireTimeoutMs_(0),
+ readSkewPartitionWithoutMapRange_(false),
+ shuffleClient_(nullptr) {}
+
+std::unique_ptr<CelebornInputStream> CelebornInputStream::empty() {
+ // Private constructor: not reachable via make_unique.
+ return std::unique_ptr<CelebornInputStream>(new CelebornInputStream());
+}
+
int CelebornInputStream::read(uint8_t* buffer, size_t offset, size_t len) {
CELEBORN_CHECK_NOT_NULL(buffer);
uint8_t* buf = buffer + offset;
diff --git a/cpp/celeborn/client/reader/CelebornInputStream.h
b/cpp/celeborn/client/reader/CelebornInputStream.h
index c333c9a45b..3af0ee47fc 100644
--- a/cpp/celeborn/client/reader/CelebornInputStream.h
+++ b/cpp/celeborn/client/reader/CelebornInputStream.h
@@ -54,9 +54,16 @@ class CelebornInputStream {
std::shared_ptr<const protocol::PartitionPushFailedBatches>
pushFailedBatches = nullptr);
+ // Returns an empty, no-op stream whose read() immediately yields -1, for a
+ // partition with no data.
+ static std::unique_ptr<CelebornInputStream> empty();
+
int read(uint8_t* buffer, size_t offset, size_t len);
private:
+ // The empty, no-op stream for empty(); members are zero/null-initialized.
+ CelebornInputStream();
+
bool fillBuffer();
bool moveToNextChunk();
diff --git a/cpp/celeborn/client/tests/CMakeLists.txt
b/cpp/celeborn/client/tests/CMakeLists.txt
index 796ecbc469..08623486b2 100644
--- a/cpp/celeborn/client/tests/CMakeLists.txt
+++ b/cpp/celeborn/client/tests/CMakeLists.txt
@@ -19,6 +19,7 @@ add_executable(
PushDataCallbackTest.cpp
PushMergedDataCallbackTest.cpp
ShuffleClientImplTest.cpp
+ CelebornInputStreamTest.cpp
CelebornInputStreamRetryTest.cpp
PushStateTest.cpp
ReviveManagerTest.cpp
diff --git a/cpp/celeborn/client/tests/CelebornInputStreamTest.cpp
b/cpp/celeborn/client/tests/CelebornInputStreamTest.cpp
new file mode 100644
index 0000000000..81299e3c26
--- /dev/null
+++ b/cpp/celeborn/client/tests/CelebornInputStreamTest.cpp
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+#include <cstdint>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+#include "celeborn/client/reader/CelebornInputStream.h"
+
+using namespace celeborn;
+using namespace celeborn::client;
+
+// empty() returns a no-op stream: read() reports EOF (-1) immediately and
stays
+// at EOF across repeated and larger reads.
+TEST(CelebornInputStreamTest, emptyStreamReadsReturnEof) {
+ auto stream = CelebornInputStream::empty();
+ ASSERT_NE(stream, nullptr);
+
+ std::vector<uint8_t> buffer(16, 0);
+ EXPECT_EQ(stream->read(buffer.data(), 0, buffer.size()), -1);
+ // No state to advance: a second read still reports EOF.
+ EXPECT_EQ(stream->read(buffer.data(), 0, buffer.size()), -1);
+ // A zero-length read is a no-op that returns 0, not EOF.
+ EXPECT_EQ(stream->read(buffer.data(), 0, 0), 0);
+}
diff --git a/cpp/celeborn/client/tests/ShuffleClientImplTest.cpp
b/cpp/celeborn/client/tests/ShuffleClientImplTest.cpp
index 35be191725..22e85fbee2 100644
--- a/cpp/celeborn/client/tests/ShuffleClientImplTest.cpp
+++ b/cpp/celeborn/client/tests/ShuffleClientImplTest.cpp
@@ -15,10 +15,15 @@
* limitations under the License.
*/
+#include <climits>
+#include <cstdint>
+#include <vector>
+
#include <gtest/gtest.h>
#include "celeborn/client/ShuffleClient.h"
#include "celeborn/client/tests/ShuffleClientTestUtils.h"
+#include "celeborn/protocol/ControlMessages.h"
using namespace celeborn;
using namespace celeborn::client;
@@ -50,6 +55,12 @@ class TestableShuffleClient : public TestShuffleClientBase {
return getPushTargetWorkerExcludeCause(location);
}
+ void seedReducerFileGroupInfo(
+ int shuffleId,
+ std::shared_ptr<protocol::GetReducerFileGroupResponse> info) {
+ setReducerFileGroupInfoForTest(shuffleId, std::move(info));
+ }
+
private:
explicit TestableShuffleClient(
const std::shared_ptr<const conf::CelebornConf>& conf)
@@ -268,3 +279,46 @@ TEST(ShuffleClientImplTest, shutdownClearsExcludedWorkers)
{
EXPECT_FALSE(client->callGetPushTargetWorkerExcludeCause(*loc).has_value());
}
+
+// Asserts the stream reports EOF immediately, i.e. it carries no data.
+static void expectEmptyStream(
+ const std::unique_ptr<CelebornInputStream>& stream) {
+ ASSERT_NE(stream, nullptr);
+ std::vector<uint8_t> buffer(8, 0);
+ EXPECT_EQ(stream->read(buffer.data(), 0, buffer.size()), -1);
+}
+
+// readPartition returns an empty (EOF) stream when the shuffle's file groups
+// hold no locations for the partition. The seeded cache makes
+// getReducerFileGroupInfo hit, so no RPC is attempted.
+TEST(ShuffleClientImplTest, readPartitionEmptyWhenFileGroupsEmpty) {
+ auto client = TestableShuffleClient::create(makeConf(/*enabled=*/false));
+ auto info = std::make_shared<protocol::GetReducerFileGroupResponse>();
+ info->status = protocol::StatusCode::SUCCESS;
+ client->seedReducerFileGroupInfo(/*shuffleId=*/1, info);
+
+ expectEmptyStream(client->readPartition(
+ /*shuffleId=*/1,
+ /*partitionId=*/0,
+ /*attemptNumber=*/0,
+ /*startMapIndex=*/0,
+ /*endMapIndex=*/INT_MAX));
+}
+
+// Same empty-stream result when the file-group map is populated but has no
+// entry for the requested partition.
+TEST(ShuffleClientImplTest, readPartitionEmptyWhenPartitionMissing) {
+ auto client = TestableShuffleClient::create(makeConf(/*enabled=*/false));
+ auto info = std::make_shared<protocol::GetReducerFileGroupResponse>();
+ info->status = protocol::StatusCode::SUCCESS;
+ info->fileGroups[7].insert(makeLocation(7, 0, "host-7", 9007));
+ client->seedReducerFileGroupInfo(/*shuffleId=*/2, info);
+
+ // Partition 0 is absent (only partition 7 has locations).
+ expectEmptyStream(client->readPartition(
+ /*shuffleId=*/2,
+ /*partitionId=*/0,
+ /*attemptNumber=*/0,
+ /*startMapIndex=*/0,
+ /*endMapIndex=*/INT_MAX));
+}