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

zaynt4606 pushed a commit to branch branch-0.7
in repository https://gitbox.apache.org/repos/asf/celeborn.git


The following commit(s) were added to refs/heads/branch-0.7 by this push:
     new 936ebd0fec [CELEBORN-2501] Fix DataPushQueue dropping tasks when there 
is no partition location map
936ebd0fec is described below

commit 936ebd0fec246bcc5039b0f67b5f54bf287e0962
Author: ForVic <[email protected]>
AuthorDate: Sun Oct 25 16:05:16 2026 +0800

    [CELEBORN-2501] Fix DataPushQueue dropping tasks when there is no partition 
location map
    
    ### What changes were proposed in this pull request?
    Minor bug fix, ran into it while running a micro benchmark.
    When `client.getPartitionLocation` returns `null`, 
`DataPushQueue.takePushTasks` takes every queued task with:
    
    ```java
    tasks.addAll(workingQueue);
    workingQueue.clear();
    ```
    
    The writer thread keeps adding to `workingQueue` while this runs. If it 
adds a task after `addAll` has copied the queue and before `clear` runs, 
`clear` removes that task even though it was never copied into `tasks`. The 
task is never pushed, and its `PushTask` buffer never goes back to 
`DataPusher`'s idle queue.
    
    This PR replaces the two calls with `workingQueue.drainTo(tasks)`, which 
removes only the tasks it returns. The branch behaved this way before 
CELEBORN-1218 (#2220), which removed each task through `iterator.remove()`.
    
    ### Why are the changes needed?
    
    Each lost task loses a pushed batch and one buffer from the idle pool. Once 
every buffer is lost, the writer blocks forever in `DataPusher.addTask` waiting 
for an idle buffer, and the push thread sleeps on an empty working queue. If 
fewer are lost, `waitOnTermination` waits forever for the idle queue to fill up 
again.
    
    `ShuffleClientImpl.getPartitionLocation` never returns `null`: it returns 
the registered map or throws. So production writers do not take this branch. 
`DummyShuffleClient` returns `null` when no reduce partition map was 
initialized, so tests and benchmarks built on it can hang.
    
    ### Does this PR resolve a correctness bug?
    
    - [ ] Yes
    
    ### Does this PR introduce _any_ user-facing change?
    
    - [ ] Yes
    
    ### How was this patch tested?
    
    - New `DataPushQueueSuiteJ#testTaskAddedWhileTakingAllTasksIsNotLost`. A 
package-private constructor lets the test pass in its own working queue. The 
queue adds a second task right after `takePushTasks` reads the queued tasks, 
which is exactly the gap between the old `addAll` and `clear`. The test checks 
that `takePushTasks` returns the first task and that the second one is still 
queued.
      - Without the fix it failed 3 runs out of 3, because the second task was 
cleared.
      - With the fix it passes.
    - Before switching to this test, a 200,000-task stress test through 
`DataPusher` with no partition map hung the old code after 90,829 pushes, with 
the writer parked in `addTask` and the push thread sleeping in `takePushTasks`.
    - Existing `DataPushQueueSuiteJ`.
    
    Closes #3895 from ForVic/ForVic/o5nq5.
    
    Authored-by: ForVic <[email protected]>
    Signed-off-by: zhengtao <[email protected]>
    
    AI-Contributed/Feature: 0/32
    AI-Contributed/UT: 0/56
    
    (cherry picked from commit d750eddeb3cfd90fca21d6d2e961b0eb53765c6f)
    Signed-off-by: zhengtao <[email protected]>
---
 .../celeborn/client/write/DataPushQueue.java       | 32 +++++++++++--
 .../celeborn/client/write/DataPushQueueSuiteJ.java | 56 ++++++++++++++++++++++
 2 files changed, 84 insertions(+), 4 deletions(-)

diff --git 
a/client/src/main/java/org/apache/celeborn/client/write/DataPushQueue.java 
b/client/src/main/java/org/apache/celeborn/client/write/DataPushQueue.java
index aec6a69b5d..71e61d38f8 100644
--- a/client/src/main/java/org/apache/celeborn/client/write/DataPushQueue.java
+++ b/client/src/main/java/org/apache/celeborn/client/write/DataPushQueue.java
@@ -23,6 +23,7 @@ import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import com.google.common.annotations.VisibleForTesting;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -62,6 +63,29 @@ public class DataPushQueue {
       int attemptId,
       int numMappers,
       int numPartitions) {
+    this(
+        conf,
+        dataPusher,
+        client,
+        shuffleId,
+        mapId,
+        attemptId,
+        numMappers,
+        numPartitions,
+        new LinkedBlockingQueue<>(conf.clientPushQueueCapacity()));
+  }
+
+  @VisibleForTesting
+  DataPushQueue(
+      CelebornConf conf,
+      DataPusher dataPusher,
+      ShuffleClient client,
+      int shuffleId,
+      int mapId,
+      int attemptId,
+      int numMappers,
+      int numPartitions,
+      LinkedBlockingQueue<PushTask> workingQueue) {
     this.shuffleId = shuffleId;
     this.numMappers = numMappers;
     this.numPartitions = numPartitions;
@@ -71,8 +95,7 @@ public class DataPushQueue {
     this.pushState = client.getPushState(mapKey);
     this.takeTaskWaitIntervalMs = conf.clientPushTakeTaskWaitIntervalMs();
     this.takeTaskMaxWaitAttempts = conf.clientPushTakeTaskMaxWaitAttempts();
-    final int capacity = conf.clientPushQueueCapacity();
-    workingQueue = new LinkedBlockingQueue<>(capacity);
+    this.workingQueue = workingQueue;
   }
 
   /*
@@ -91,8 +114,9 @@ public class DataPushQueue {
       Map<Integer, PartitionLocation> partitionLocationMap =
           client.getPartitionLocation(shuffleId, numMappers, numPartitions);
       if (partitionLocationMap == null) {
-        tasks.addAll(workingQueue);
-        workingQueue.clear();
+        // drainTo removes exactly the tasks it returns, so a task added 
concurrently by the
+        // writer thread is not dropped as it would be by addAll followed by 
clear.
+        workingQueue.drainTo(tasks);
       } else {
         Iterator<PushTask> iterator = workingQueue.iterator();
         while (iterator.hasNext()) {
diff --git 
a/client/src/test/java/org/apache/celeborn/client/write/DataPushQueueSuiteJ.java
 
b/client/src/test/java/org/apache/celeborn/client/write/DataPushQueueSuiteJ.java
index 841e04eee0..667d88b9bf 100644
--- 
a/client/src/test/java/org/apache/celeborn/client/write/DataPushQueueSuiteJ.java
+++ 
b/client/src/test/java/org/apache/celeborn/client/write/DataPushQueueSuiteJ.java
@@ -20,15 +20,19 @@ package org.apache.celeborn.client.write;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.atomic.LongAdder;
 
 import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
+import org.mockito.Mockito;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -178,6 +182,58 @@ public class DataPushQueueSuiteJ {
     client.shutdown();
   }
 
+  @Test
+  public void testTaskAddedWhileTakingAllTasksIsNotLost() throws Exception {
+    CelebornConf conf = new CelebornConf();
+    final File tempFile = new File(tempDir, UUID.randomUUID().toString());
+    // No reduce partition map is initialized, so getPartitionLocation returns 
null and
+    // takePushTasks takes every queued task.
+    DummyShuffleClient client = new DummyShuffleClient(conf, tempFile);
+    DataPusher dataPusher = Mockito.mock(DataPusher.class);
+    Mockito.when(dataPusher.stillRunning()).thenReturn(true);
+
+    PushTask queuedTask = new PushTask(4);
+    PushTask concurrentTask = new PushTask(4);
+    // Adds concurrentTask right after takePushTasks reads the queued tasks, 
as the writer thread
+    // can. toArray is how ArrayList.addAll reads the queue, drainTo is how it 
is read with the fix.
+    LinkedBlockingQueue<PushTask> workingQueue =
+        new LinkedBlockingQueue<PushTask>(conf.clientPushQueueCapacity()) {
+          private boolean added = false;
+
+          private void addConcurrentTaskOnce() {
+            if (!added) {
+              added = true;
+              super.offer(concurrentTask);
+            }
+          }
+
+          @Override
+          public Object[] toArray() {
+            Object[] tasks = super.toArray();
+            addConcurrentTaskOnce();
+            return tasks;
+          }
+
+          @Override
+          public int drainTo(Collection<? super PushTask> c) {
+            int n = super.drainTo(c);
+            addConcurrentTaskOnce();
+            return n;
+          }
+        };
+    DataPushQueue dataPushQueue =
+        new DataPushQueue(conf, dataPusher, client, 0, 0, 0, 1, 1, 
workingQueue);
+
+    try {
+      Assert.assertTrue(dataPushQueue.addPushTask(queuedTask));
+      Assert.assertEquals(Collections.singletonList(queuedTask), 
dataPushQueue.takePushTasks());
+      // The task added while taking must stay queued for the next 
takePushTasks call.
+      Assert.assertEquals(Collections.singletonList(concurrentTask), new 
ArrayList<>(workingQueue));
+    } finally {
+      client.shutdown();
+    }
+  }
+
   public static byte[] intToBytes(int value) {
     byte[] src = new byte[4];
     src[0] = (byte) (value & 0xFF);

Reply via email to