You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@celeborn.apache.org by za...@apache.org on 2026/10/10 09:57:20 UTC

(celeborn) branch main updated: [CELEBORN-2434] Clamp endMapIndex in sliceSortedBufferByMapRange

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

zaynt4606 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 c090945514 [CELEBORN-2434] Clamp endMapIndex in sliceSortedBufferByMapRange
c090945514 is described below

commit c090945514d39f888ac27a706e4807df7dd815b8
Author: yew1eb <ye...@gmail.com>
AuthorDate: Sat Oct 10 17:56:56 2026 +0800

    [CELEBORN-2434] Clamp endMapIndex in sliceSortedBufferByMapRange
    
    ### What changes were proposed in this pull request?
    
    Add the missing `endMapIndex == Integer.MAX_VALUE` clamp in `ShuffleBlockInfoUtils.sliceSortedBufferByMapRange`, mirroring the existing guard in the sibling method `getChunkOffsetsFromShuffleBlockInfos`. Also add a unit test covering the full re-read (`Integer.MAX_VALUE`) case.
    
    ### Why are the changes needed?
    
    When a memory-stored shuffle partition is read once with a map range (e.g. AQE skew join) and then re-read in full (`endIndex = Int.MaxValue`, e.g. Spark ReusedExchange), `FetchHandler.handleReduceOpenStreamInternal` passes the raw `Int.MaxValue` into `getSortedFileInfo` because `FileInfo.addStream` returns false for the already-sorted file. The memory branch then loops `startMapIndex..Integer.MAX_VALUE` in `sliceSortedBufferByMapRange` — billions of `TreeMap.get` calls on the Netty f [...]
    
    The disk path is not affected (it goes through the already-clamped `getChunkOffsetsFromShuffleBlockInfos`).
    
    Symptoms / evidence:
    
    • MemoryStorageReusedExchangeSuite (a single tiny test with 1000 rows) takes 28–54 minutes instead of ~1 minute, both in CI and locally.
    • Client side repeatedly logs BatchOpenStream for N cost 240006ms (exactly the 240s fetch timeout), followed by retries that stall again.
    • Client logs Connection ... has been quiet for 4.0 m while there are outstanding requests against both workers' fetch ports.
    • A thread dump taken while hung shows the fetch event loop spinning:
    
    ```
      "fetch-server-34-2" RUNNABLE
        at java.util.TreeMap.getEntry(TreeMap.java:359)
        at java.util.TreeMap.get(TreeMap.java:278)
        at ShuffleBlockInfoUtils.sliceSortedBufferByMapRange(ShuffleBlockInfoUtils.java:140)
        at PartitionFilesSorter.getSortedFileInfo(PartitionFilesSorter.java:224)
        at FetchHandler.handleReduceOpenStreamInternal(FetchHandler.scala:277)
    ```
    
    In production this means: memory storage + a range read followed by a full re-read of the same partition can wedge a worker's fetch event loop for an
    unbounded time (the loop count scales with Int.MaxValue, multiplied by the number of files in the batch request).
    
    ### Does this PR resolve a correctness bug?
    
    - [ ] Yes
    
    ### Does this PR introduce _any_ user-facing change?
    
    - [ ] Yes
    
    ### How was this patch tested?
    
    - Added `testSliceSortedBufferByMapRangeWithMaxEndIndex` to `ShuffleBlockInfoUtilsTest`, asserting a full re-read with `Integer.MAX_VALUE` produces exactly the same result as an explicit full-range read.
    - `ShuffleBlockInfoUtilsTest`: all 6 tests pass.
    
    Closes #3815 from yew1eb/CELEBORN-2434.
    
    Authored-by: yew1eb <ye...@gmail.com>
    Signed-off-by: zhengtao <sh...@alibaba-inc.com>
    
    AI-Contributed/Feature: 0/7
    AI-Contributed/UT: 0/60
---
 .../common/util/ShuffleBlockInfoUtils.java         |  7 ++-
 .../common/util/ShuffleBlockInfoUtilsTest.java     | 60 ++++++++++++++++++++++
 2 files changed, 66 insertions(+), 1 deletion(-)

diff --git a/common/src/main/java/org/apache/celeborn/common/util/ShuffleBlockInfoUtils.java b/common/src/main/java/org/apache/celeborn/common/util/ShuffleBlockInfoUtils.java
index d62bf95d39..9c784d3e76 100644
--- a/common/src/main/java/org/apache/celeborn/common/util/ShuffleBlockInfoUtils.java
+++ b/common/src/main/java/org/apache/celeborn/common/util/ShuffleBlockInfoUtils.java
@@ -136,7 +136,12 @@ public class ShuffleBlockInfoUtils {
     int offset = 0;
     int length = 0;
     boolean blockBoundary = true;
-    for (int i = startMapIndex; i < endMapIndex; i++) {
+    int maxMapIndex = endMapIndex;
+    if (endMapIndex == Integer.MAX_VALUE) {
+      // not a range read
+      maxMapIndex = indexMap.keySet().stream().max(Integer::compareTo).get() + 1;
+    }
+    for (int i = startMapIndex; i < maxMapIndex; i++) {
       List<ShuffleBlockInfo> blockInfos = indexMap.get(i);
       if (blockInfos != null) {
         for (ShuffleBlockInfo blockInfo : blockInfos) {
diff --git a/common/src/test/java/org/apache/celeborn/common/util/ShuffleBlockInfoUtilsTest.java b/common/src/test/java/org/apache/celeborn/common/util/ShuffleBlockInfoUtilsTest.java
index 72d7e6af54..dea55d3a6c 100644
--- a/common/src/test/java/org/apache/celeborn/common/util/ShuffleBlockInfoUtilsTest.java
+++ b/common/src/test/java/org/apache/celeborn/common/util/ShuffleBlockInfoUtilsTest.java
@@ -143,4 +143,64 @@ public class ShuffleBlockInfoUtilsTest {
         "Target buffer should remain empty with an empty indexMap",
         targetByteBuf.numComponents() == 0);
   }
+
+  @Test
+  public void testSliceSortedBufferByMapRangeWithMaxEndIndex() {
+    Map<Integer, List<ShuffleBlockInfo>> indexMap = new HashMap<>();
+
+    indexMap.put(0, Arrays.asList(new ShuffleBlockInfo(0, 50), new ShuffleBlockInfo(50, 30)));
+    indexMap.put(1, Arrays.asList(new ShuffleBlockInfo(0, 20), new ShuffleBlockInfo(20, 30)));
+
+    for (ShuffleBlockInfo blockInfo :
+        indexMap.values().stream().flatMap(List::stream).toArray(ShuffleBlockInfo[]::new)) {
+      byte[] data = new byte[(int) blockInfo.length];
+      Arrays.fill(data, (byte) blockInfo.offset); // Fill data with offset value for simplicity
+      sortedByteBuf.addComponents(Unpooled.wrappedBuffer(data));
+    }
+
+    // A full re-read passes endMapIndex == Integer.MAX_VALUE. It must be clamped to
+    // maxMapIndex + 1 instead of looping up to Integer.MAX_VALUE, and must produce
+    // exactly the same result as an explicit full-range read.
+    CompositeByteBuf targetByteBufWithMaxEndIndex = Unpooled.compositeBuffer();
+    ShuffleBlockInfoUtils.sliceSortedBufferByMapRange(
+        0,
+        Integer.MAX_VALUE,
+        indexMap,
+        sortedByteBuf,
+        targetByteBufWithMaxEndIndex,
+        shuffleChunkSize);
+
+    ShuffleBlockInfoUtils.sliceSortedBufferByMapRange(
+        0, 2, indexMap, sortedByteBuf, targetByteBuf, shuffleChunkSize);
+
+    Assert.assertEquals(
+        "Unexpected number of components in target buffer",
+        targetByteBuf.numComponents(),
+        targetByteBufWithMaxEndIndex.numComponents());
+    // addComponent does not advance the target's writerIndex, so readableBytes() on the
+    // composite buffers is always 0. Compare the per-component bytes instead.
+    long expectedBytes = 50 + 30 + 20 + 30;
+    Assert.assertEquals(
+        "Unexpected sliced bytes for full re-read",
+        expectedBytes,
+        totalComponentBytes(targetByteBufWithMaxEndIndex));
+    Assert.assertEquals(
+        "Full re-read must produce the same bytes as an explicit full-range read",
+        totalComponentBytes(targetByteBuf),
+        totalComponentBytes(targetByteBufWithMaxEndIndex));
+    for (int i = 0; i < targetByteBuf.numComponents(); i++) {
+      Assert.assertEquals(
+          "Component " + i + " content mismatch",
+          targetByteBuf.component(i),
+          targetByteBufWithMaxEndIndex.component(i));
+    }
+  }
+
+  private static long totalComponentBytes(CompositeByteBuf buf) {
+    long total = 0;
+    for (int i = 0; i < buf.numComponents(); i++) {
+      total += buf.component(i).readableBytes();
+    }
+    return total;
+  }
 }