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 fca9da2368 [CELEBORN-2482] Report pushes failed by push-timeout 
channel close as push timeout instead of connection exception
fca9da2368 is described below

commit fca9da23687c4d484f449abce9f87ed66eaf2ae1
Author: afterincomparableyum 
<[email protected]>
AuthorDate: Tue Oct 20 11:32:35 2026 +0800

    [CELEBORN-2482] Report pushes failed by push-timeout channel close as push 
timeout instead of connection exception
    
    ### What changes were proposed in this pull request?
    
    When a push request times out and its timeout handler closes the channel 
(CELEBORN-2458), `TransportResponseHandler` now records the timeout status 
(`PUSH_DATA_TIMEOUT_PRIMARY`/`REPLICA`) before the close. The other pushes on 
that channel that fail because of the close are reported with this status 
instead of as connection exceptions. That covers both `channelInactive` and 
write failures on the closed channel.
    
    ### Why are the changes needed?
    
    Since CELEBORN-2458, a push timeout closes the channel. The other pushes 
still outstanding on it are then failed with `Connection from ... closed` (or 
`Failed to send request ...`), which `ExceptionUtils.connectFail` maps to 
`PUSH_DATA_CONNECTION_EXCEPTION_PRIMARY`/`REPLICA`. These collateral failures 
overwrite the worker's `PUSH_DATA_TIMEOUT_*` entry in the excluded workers, so 
a worker that stopped responding gets misreported as a connection failure. This 
also makes `PushDataTimeout [...]
    
    ```
    - celeborn spark integration test - pushdata timeout will add to 
pushExcludedWorkers *** FAILED ***
      PUSH_DATA_CONNECTION_EXCEPTION_PRIMARY did not equal 
PUSH_DATA_TIMEOUT_PRIMARY, and PUSH_DATA_CONNECTION_EXCEPTION_PRIMARY did not 
equal PUSH_DATA_TIMEOUT_REPLICA (PushDataTimeoutTest.scala:157)
    ```
    
    In the [failed 
run](https://github.com/apache/celeborn/actions/runs/38396707096/job/115293643837),
 a push to a worker timed out (`PUSH_DATA_TIMEOUT_PRIMARY`). In the same 
millisecond, `Still have 1 requests outstanding when connection from ... is 
closed` failed a second push on that channel, and the worker was then reported 
as `PUSH_DATA_CONNECTION_EXCEPTION_PRIMARY`.
    
    ### Does this PR resolve a correctness bug?
    
    - [ ] Yes
    
    ### Does this PR introduce _any_ user-facing change?
    
    - [ ] Yes
    
    ### How was this patch tested?
    
    - New tests in `TransportResponseHandlerSuiteJ`. The new push-timeout test 
fails without this change.
      - A push timeout that closes the channel fails the other outstanding 
pushes, and later write failures, with the timeout status.
      - A channel close without a push timeout still fails pushes with the 
connection exception.
    - `TransportResponseHandlerSuiteJ` (10 tests) and 
`TransportClientFactorySuiteJ` (11 tests): passed.
    - `PushDataTimeoutTest` with Spark 3.5: 5 tests passed.
    
    Closes #3876 from afterincomparableyum/push-timeout-close-status.
    
    Authored-by: afterincomparableyum 
<[email protected]>
    Signed-off-by: zhengtao <[email protected]>
    
    AI-Contributed/Feature: 0/42
    AI-Contributed/UT: 0/100
---
 .../network/client/TransportResponseHandler.java   |  38 +++++---
 .../celeborn/common/write/PushRequestInfo.java     |   4 +
 .../network/TransportResponseHandlerSuiteJ.java    | 100 +++++++++++++++++++++
 3 files changed, 132 insertions(+), 10 deletions(-)

diff --git 
a/common/src/main/java/org/apache/celeborn/common/network/client/TransportResponseHandler.java
 
b/common/src/main/java/org/apache/celeborn/common/network/client/TransportResponseHandler.java
index 5074a5cc2e..92f953fb0a 100644
--- 
a/common/src/main/java/org/apache/celeborn/common/network/client/TransportResponseHandler.java
+++ 
b/common/src/main/java/org/apache/celeborn/common/network/client/TransportResponseHandler.java
@@ -59,6 +59,8 @@ public class TransportResponseHandler extends 
MessageHandler<ResponseMessage> {
   private final ConcurrentHashMap<Long, RpcResponseCallback> outstandingRpcs;
   private final ConcurrentHashMap<Long, PushRequestInfo> outstandingPushes;
 
+  private volatile StatusCode pushTimeoutCloseStatus;
+
   /** Records the time (in system nanoseconds) that the last fetch or RPC 
request was sent. */
   private final AtomicLong timeOfLastRequestNs;
 
@@ -129,6 +131,7 @@ public class TransportResponseHandler extends 
MessageHandler<ResponseMessage> {
   }
 
   public void failExpiredPushRequest() {
+    StatusCode timeoutStatus = pushTimeoutStatus();
     long currentTime = System.currentTimeMillis();
     Iterator<Map.Entry<Long, PushRequestInfo>> iter = 
outstandingPushes.entrySet().iterator();
     while (iter.hasNext()) {
@@ -136,18 +139,15 @@ public class TransportResponseHandler extends 
MessageHandler<ResponseMessage> {
       if (entry.getValue().dueTime <= currentTime) {
         PushRequestInfo info = removePushRequest(entry.getKey());
         if (info != null) {
+          if (timeoutStatus != null && info.hasTimeoutHandler()) {
+            pushTimeoutCloseStatus = timeoutStatus;
+          }
           if (info.channelFuture != null) {
             info.channelFuture.cancel(true);
           }
           info.handleTimeout();
-          String module = conf.getModuleName();
-          // When module name equals to DATA_MODULE, mean shuffle client push 
data, else means
-          // do data replication.
-          if (TransportModuleConstants.DATA_MODULE.equals(module)) {
-            info.callback.onFailure(new 
CelebornIOException(StatusCode.PUSH_DATA_TIMEOUT_PRIMARY));
-          } else if (TransportModuleConstants.PUSH_MODULE.equals(module)
-              || TransportModuleConstants.REPLICATE_MODULE.equals(module)) {
-            info.callback.onFailure(new 
CelebornIOException(StatusCode.PUSH_DATA_TIMEOUT_REPLICA));
+          if (timeoutStatus != null) {
+            info.callback.onFailure(new CelebornIOException(timeoutStatus));
           }
           info.channelFuture = null;
           info.callback = null;
@@ -156,6 +156,24 @@ public class TransportResponseHandler extends 
MessageHandler<ResponseMessage> {
     }
   }
 
+  private StatusCode pushTimeoutStatus() {
+    String module = conf.getModuleName();
+    // When module name equals to DATA_MODULE, mean shuffle client push data, 
else means
+    // do data replication.
+    if (TransportModuleConstants.DATA_MODULE.equals(module)) {
+      return StatusCode.PUSH_DATA_TIMEOUT_PRIMARY;
+    } else if (TransportModuleConstants.PUSH_MODULE.equals(module)
+        || TransportModuleConstants.REPLICATE_MODULE.equals(module)) {
+      return StatusCode.PUSH_DATA_TIMEOUT_REPLICA;
+    }
+    return null;
+  }
+
+  private Throwable pushFailureCause(Throwable cause) {
+    StatusCode timeoutStatus = pushTimeoutCloseStatus;
+    return timeoutStatus == null ? cause : new 
CelebornIOException(timeoutStatus, cause);
+  }
+
   public void failExpiredFetchRequest() {
     long currentTime = System.currentTimeMillis();
     Iterator<Map.Entry<StreamChunkSlice, FetchRequestInfo>> iter =
@@ -256,7 +274,7 @@ public class TransportResponseHandler extends 
MessageHandler<ResponseMessage> {
         long requestId = pushRequestIter.next();
         PushRequestInfo info = removePushRequest(requestId);
         if (info != null) {
-          info.callback.onFailure(cause);
+          info.callback.onFailure(pushFailureCause(cause));
         }
       } catch (Exception e) {
         logger.warn("RpcResponseCallback.onFailure throws exception", e);
@@ -437,7 +455,7 @@ public class TransportResponseHandler extends 
MessageHandler<ResponseMessage> {
     if (info != null) {
       RpcResponseCallback callback = info.callback;
       if (callback != null) {
-        callback.onFailure(new CelebornIOException(errorMsg, cause));
+        callback.onFailure(pushFailureCause(new CelebornIOException(errorMsg, 
cause)));
       } else {
         logger.warn(
             "PushRequestInfo {} callback is null when handle push request 
failure", pushRequestId);
diff --git 
a/common/src/main/java/org/apache/celeborn/common/write/PushRequestInfo.java 
b/common/src/main/java/org/apache/celeborn/common/write/PushRequestInfo.java
index 5d543aa05a..d2384dce50 100644
--- a/common/src/main/java/org/apache/celeborn/common/write/PushRequestInfo.java
+++ b/common/src/main/java/org/apache/celeborn/common/write/PushRequestInfo.java
@@ -41,6 +41,10 @@ public class PushRequestInfo {
     this.channelFuture = future;
   }
 
+  public boolean hasTimeoutHandler() {
+    return timeoutHandler != null;
+  }
+
   public void handleTimeout() {
     if (timeoutHandler != null) {
       timeoutHandler.run();
diff --git 
a/common/src/test/java/org/apache/celeborn/common/network/TransportResponseHandlerSuiteJ.java
 
b/common/src/test/java/org/apache/celeborn/common/network/TransportResponseHandlerSuiteJ.java
index 8b6adece82..242016a0d3 100644
--- 
a/common/src/test/java/org/apache/celeborn/common/network/TransportResponseHandlerSuiteJ.java
+++ 
b/common/src/test/java/org/apache/celeborn/common/network/TransportResponseHandlerSuiteJ.java
@@ -18,12 +18,17 @@
 package org.apache.celeborn.common.network;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.*;
 
 import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
 
 import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.DefaultChannelPromise;
 import io.netty.channel.local.LocalChannel;
+import io.netty.util.concurrent.ImmediateEventExecutor;
 import org.junit.Test;
 import org.mockito.InOrder;
 import org.mockito.Mockito;
@@ -207,4 +212,99 @@ public class TransportResponseHandlerSuiteJ {
                     
error.getMessage().startsWith(StatusCode.PUSH_DATA_TIMEOUT_REPLICA.name())));
     assertEquals(0, handler.numOutstandingRequests());
   }
+
+  @Test
+  public void failOtherPushesWithTimeoutStatusWhenPushTimeoutClosesChannel() {
+    TransportResponseHandler handler =
+        new TransportResponseHandler(
+            Utils.fromCelebornConf(new CelebornConf(), 
TransportModuleConstants.DATA_MODULE, 8),
+            new LocalChannel());
+    RpcResponseCallback expiredCallback = mock(RpcResponseCallback.class);
+    RpcResponseCallback pendingCallback = mock(RpcResponseCallback.class);
+    RpcResponseCallback writeFailedCallback = mock(RpcResponseCallback.class);
+    handler.addPushRequest(
+        1,
+        new PushRequestInfo(
+            System.currentTimeMillis() - 1, expiredCallback, 
handler::channelInactive));
+    handler.addPushRequest(
+        2,
+        new PushRequestInfo(
+            System.currentTimeMillis() + 30000L, pendingCallback, 
mock(Runnable.class)));
+
+    handler.failExpiredPushRequest();
+
+    
verify(expiredCallback).onFailure(failureStartingWith(StatusCode.PUSH_DATA_TIMEOUT_PRIMARY));
+    
verify(pendingCallback).onFailure(failureStartingWith(StatusCode.PUSH_DATA_TIMEOUT_PRIMARY));
+    assertEquals(0, handler.numOutstandingRequests());
+
+    handler.addPushRequest(
+        3,
+        new PushRequestInfo(
+            System.currentTimeMillis() + 30000L, writeFailedCallback, 
mock(Runnable.class)));
+    handler.handlePushFailure(
+        3, "Failed to send request PUSH 3 to <unknown remote>", new 
ClosedChannelException());
+    verify(writeFailedCallback)
+        .onFailure(failureStartingWith(StatusCode.PUSH_DATA_TIMEOUT_PRIMARY));
+  }
+
+  @Test
+  public void 
failOtherPushesWithTimeoutStatusWhenCancelledWriteClosesChannel() {
+    TransportResponseHandler handler =
+        new TransportResponseHandler(
+            Utils.fromCelebornConf(new CelebornConf(), 
TransportModuleConstants.DATA_MODULE, 8),
+            new LocalChannel());
+    RpcResponseCallback expiredCallback = mock(RpcResponseCallback.class);
+    RpcResponseCallback pendingCallback = mock(RpcResponseCallback.class);
+    PushRequestInfo expiredInfo =
+        new PushRequestInfo(System.currentTimeMillis() - 1, expiredCallback, 
mock(Runnable.class));
+    // A still-queued write: cancelling it notifies the listener inline, which 
closes the channel
+    // like PushChannelListener does on write failure.
+    ChannelPromise pushFuture =
+        new DefaultChannelPromise(new LocalChannel(), 
ImmediateEventExecutor.INSTANCE);
+    pushFuture.addListener(
+        future -> {
+          if (!future.isSuccess()) {
+            handler.channelInactive();
+            handler.handlePushFailure(1, "Failed to send request PUSH 1", 
future.cause());
+          }
+        });
+    expiredInfo.setChannelFuture(pushFuture);
+    handler.addPushRequest(1, expiredInfo);
+    handler.addPushRequest(
+        2,
+        new PushRequestInfo(
+            System.currentTimeMillis() + 30000L, pendingCallback, 
mock(Runnable.class)));
+
+    handler.failExpiredPushRequest();
+
+    assertTrue(pushFuture.isCancelled());
+    
verify(expiredCallback).onFailure(failureStartingWith(StatusCode.PUSH_DATA_TIMEOUT_PRIMARY));
+    
verify(pendingCallback).onFailure(failureStartingWith(StatusCode.PUSH_DATA_TIMEOUT_PRIMARY));
+    assertEquals(0, handler.numOutstandingRequests());
+  }
+
+  @Test
+  public void 
failPushesWithConnectionExceptionWhenChannelClosesWithoutPushTimeout() {
+    TransportResponseHandler handler =
+        new TransportResponseHandler(
+            Utils.fromCelebornConf(new CelebornConf(), 
TransportModuleConstants.DATA_MODULE, 8),
+            new LocalChannel());
+    RpcResponseCallback callback = mock(RpcResponseCallback.class);
+    handler.addPushRequest(
+        1,
+        new PushRequestInfo(System.currentTimeMillis() + 30000L, callback, 
mock(Runnable.class)));
+
+    handler.channelInactive();
+
+    verify(callback)
+        .onFailure(
+            Mockito.<Throwable>argThat(
+                error ->
+                    error.getMessage().startsWith("Connection from ")
+                        && error.getMessage().endsWith(" closed")));
+  }
+
+  private static Throwable failureStartingWith(StatusCode statusCode) {
+    return Mockito.<Throwable>argThat(error -> 
error.getMessage().startsWith(statusCode.name()));
+  }
 }

Reply via email to