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

SteNicholas 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 1c807d99a [CELEBORN-2330] Fix HA master bootstrap redirect handling
1c807d99a is described below

commit 1c807d99a16e59fca0022b1524ec158bfa869514
Author: Chao Sun <[email protected]>
AuthorDate: Mon May 25 13:59:36 2026 +0800

    [CELEBORN-2330] Fix HA master bootstrap redirect handling
    
    ## Why are the changes needed?
    
    In HA mode, clients can hit a failover window where the master they contact 
is no longer the leader and returns a `MasterNotLeaderException` with a 
suggested leader address. The client-facing symptom is an RPC failure surfaced 
as:
    
    `CelebornException: Exception thrown in awaitResult`
    
    The redirect signal is still present underneath that wrapper, but the 
existing bootstrap and retry logic does not consistently preserve and follow 
it. In particular:
    
    - bootstrap-time redirects can be treated like generic connection failures 
instead of explicit leader hints
    - suggested leaders can themselves redirect again or fail setup
    - after such failures, the client may not continue cleanly to the remaining 
configured masters
    
    The consequence is that a client can fail to establish a master connection 
during HA leader transitions even when a reachable leader or another configured 
master is available. That turns a recoverable redirect/failover event into an 
avoidable client-visible failure and makes rolling upgrades or leader changes 
noisier than necessary.
    
    ## What changes were proposed in this PR?
    
    This port brings the HA redirect handling fix from `openai/celeborn#70` 
onto upstream `main`.
    
    When a master tells the client which leader to use, the client now keeps 
that redirect information even if it is wrapped inside another exception, and 
it actively follows the suggested leader during bootstrap and failover. If that 
suggested leader points to another leader, the client can continue along that 
redirect chain instead of giving up too early.
    
    If the redirect path is no longer useful, for example because the suggested 
leader cannot be reached, no leader is currently presented, or redirects start 
looping, the client falls back to the remaining configured masters and keeps 
searching for a usable endpoint. The retry scan also advances correctly after 
endpoint setup fails, so one bad redirect does not prevent the client from 
trying the next viable master.
    
    The PR also adds focused HA tests that cover the bootstrap redirect cases, 
chained redirects, redirect cycles, and fallback to configured masters.
    
    ## How was this PR tested?
    
    - `build/mvn test -pl common -am -Dtest=MasterClientSuiteJ 
-DwildcardSuites=org.apache.celeborn.common.client.__NoSuchSuite__`
    
    Closes #3691 from sunchao/dev/chao/codex/port-pr70-to-oss-main.
    
    Authored-by: Chao Sun <[email protected]>
    Signed-off-by: SteNicholas <[email protected]>
---
 .../celeborn/common/client/MasterClient.java       |  78 ++++++-
 .../celeborn/common/client/MasterClientSuiteJ.java | 259 ++++++++++++++++++++-
 2 files changed, 326 insertions(+), 11 deletions(-)

diff --git 
a/common/src/main/java/org/apache/celeborn/common/client/MasterClient.java 
b/common/src/main/java/org/apache/celeborn/common/client/MasterClient.java
index 626c112a5..e57460c49 100644
--- a/common/src/main/java/org/apache/celeborn/common/client/MasterClient.java
+++ b/common/src/main/java/org/apache/celeborn/common/client/MasterClient.java
@@ -169,16 +169,18 @@ public class MasterClient {
   }
 
   private boolean shouldRetry(@Nullable RpcEndpointRef oldRef, Throwable e) {
-    // It will always throw celeborn exception , so we need to get the cause
-    // 'CelebornException: Exception thrown in awaitResult'
-    if (e.getCause() instanceof MasterNotLeaderException) {
-      MasterNotLeaderException exception = (MasterNotLeaderException) 
e.getCause();
+    // A redirect can arrive wrapped by awaitResult or by endpoint setup 
during bootstrap.
+    // Search the full cause chain so both paths can retry against the 
suggested leader.
+    MasterNotLeaderException exception = findMasterNotLeaderException(e);
+    if (exception != null) {
       String leaderAddr =
           isWorker
               ? exception.getSuggestedInternalLeaderAddress()
               : exception.getSuggestedLeaderAddress();
       if (!leaderAddr.equals(MasterNotLeaderException.LEADER_NOT_PRESENTED)) {
-        setRpcEndpointRef(leaderAddr);
+        if (!setRpcEndpointRef(leaderAddr)) {
+          resetRpcEndpointRef(oldRef);
+        }
       } else {
         LOG.warn("Master leader is not present currently, please check 
masters' status!");
         resetRpcEndpointRef(oldRef);
@@ -191,13 +193,55 @@ public class MasterClient {
     return false;
   }
 
-  private void setRpcEndpointRef(String masterEndpoint) {
+  @Nullable
+  private MasterNotLeaderException findMasterNotLeaderException(Throwable 
throwable) {
+    Throwable current = throwable;
+    while (current != null) {
+      if (current instanceof MasterNotLeaderException) {
+        return (MasterNotLeaderException) current;
+      }
+      current = current.getCause();
+    }
+    return null;
+  }
+
+  private boolean setRpcEndpointRef(String masterEndpoint) {
     // This method should never care newer or old value, we just set the 
suggested master endpoint.
     // If an error occurs when setting the suggested Master, it means that the 
Master may be down.
     // At this time, we just set `rpcEndpointRef` to null. Then next time, we 
will re-select the
     // Master and get the correct leader.
-    rpcEndpointRef.set(setupEndpointRef(masterEndpoint));
-    LOG.info("Fail over to master {}.", masterEndpoint);
+    String nextMasterEndpoint = masterEndpoint;
+    Set<String> triedMasterEndpoints = new HashSet<>();
+    while (triedMasterEndpoints.add(nextMasterEndpoint)) {
+      try {
+        RpcEndpointRef endpointRef = setupEndpointRef(nextMasterEndpoint);
+        if (endpointRef != null) {
+          rpcEndpointRef.set(endpointRef);
+          LOG.info("Fail over to master {}.", nextMasterEndpoint);
+          return true;
+        }
+        break;
+      } catch (RuntimeException e) {
+        MasterNotLeaderException exception = findMasterNotLeaderException(e);
+        if (exception == null) {
+          break;
+        }
+
+        String leaderAddr =
+            isWorker
+                ? exception.getSuggestedInternalLeaderAddress()
+                : exception.getSuggestedLeaderAddress();
+        if (MasterNotLeaderException.LEADER_NOT_PRESENTED.equals(leaderAddr)) {
+          break;
+        }
+        nextMasterEndpoint = leaderAddr;
+      }
+    }
+    rpcEndpointRef.set(null);
+    LOG.info(
+        "Fail over to master {} failed during endpoint setup; will retry with 
another master.",
+        masterEndpoint);
+    return false;
   }
 
   private void resetRpcEndpointRef(@Nullable RpcEndpointRef oldRef) {
@@ -225,6 +269,8 @@ public class MasterClient {
    * @param currentIndex current attempt master address index.
    * @throws IllegalStateException If after several attempts, the non-empty 
RpcEndpointRef still
    *     cannot be obtained.
+   * @throws RuntimeException If endpoint setup receives a leader redirect 
that should be retried by
+   *     the outer send loop.
    * @return non-empty RpcEndpointRef.
    */
   private RpcEndpointRef getOrSetupRpcEndpointRef(AtomicInteger currentIndex) {
@@ -241,7 +287,13 @@ public class MasterClient {
     if (endpointRef == null) {
       int index = currentIndex.get();
       do {
-        RpcEndpointRef tempEndpointRef = 
setupEndpointRef(activeMasterEndpoints.get(index));
+        RpcEndpointRef tempEndpointRef;
+        try {
+          tempEndpointRef = setupEndpointRef(activeMasterEndpoints.get(index));
+        } catch (RuntimeException e) {
+          currentIndex.set((index + 1) % activeMasterEndpoints.size());
+          throw e;
+        }
         if (rpcEndpointRef.compareAndSet(null, tempEndpointRef)) {
           index = (index + 1) % activeMasterEndpoints.size();
         }
@@ -269,6 +321,14 @@ public class MasterClient {
           rpcEnv.setupEndpointRef(
               RpcAddress.fromHostAndPort(endpoint), 
masterEndpointResolver.masterEndpointName());
     } catch (Exception e) {
+      MasterNotLeaderException exception = findMasterNotLeaderException(e);
+      if (exception != null
+          && !MasterNotLeaderException.LEADER_NOT_PRESENTED.equals(
+              isWorker
+                  ? exception.getSuggestedInternalLeaderAddress()
+                  : exception.getSuggestedLeaderAddress())) {
+        throw new RuntimeException(e);
+      }
       // Catch all exceptions. Because we don't care whether this exception is 
IOException or
       // TimeoutException or other exceptions, so we just try to connect to 
host:port, if fail,
       // we try next address.
diff --git 
a/common/src/test/java/org/apache/celeborn/common/client/MasterClientSuiteJ.java
 
b/common/src/test/java/org/apache/celeborn/common/client/MasterClientSuiteJ.java
index bb74d99b2..26ad11c43 100644
--- 
a/common/src/test/java/org/apache/celeborn/common/client/MasterClientSuiteJ.java
+++ 
b/common/src/test/java/org/apache/celeborn/common/client/MasterClientSuiteJ.java
@@ -245,6 +245,257 @@ public class MasterClientSuiteJ {
     checkOneMasterAskFailedInHA(new RpcTimeoutException("test", new 
TimeoutException("test")));
   }
 
+  @Test
+  public void testBootstrapMasterNotLeaderRedirectsToSuggestedLeaderInHA() {
+    final CelebornConf conf =
+        conf()
+            .set(CelebornConf.HA_ENABLED().key(), "true")
+            .set(CelebornConf.MASTER_ENDPOINTS().key(), 
"host1:9097,host3:9097")
+            .set(CelebornConf.MASTER_CLIENT_MAX_RETRIES().key(), "5");
+
+    final RpcEndpointRef master2 = Mockito.mock(RpcEndpointRef.class);
+    Mockito.doReturn(Future$.MODULE$.successful(mockResponse))
+        .when(master2)
+        .ask(Mockito.any(), Mockito.any(), Mockito.any());
+
+    Mockito.doAnswer(
+            invocation -> {
+              RpcAddress address = invocation.getArgument(0, RpcAddress.class);
+              switch (address.host()) {
+                case "host1":
+                  throw new RuntimeException(
+                      new MasterNotLeaderException("host1:9097", "host2:9097", 
null));
+                case "host2":
+                  return master2;
+                case "host3":
+                  throw new AssertionError("Should follow the suggested leader 
before host3.");
+                default:
+                  fail("Should use master host1/host2:" + masterPort + ", but 
use " + address);
+              }
+              return null;
+            })
+        .when(rpcEnv)
+        .setupEndpointRef(Mockito.any(RpcAddress.class), Mockito.anyString());
+
+    MasterClient client = new MasterClient(rpcEnv, conf, false);
+    HeartbeatFromWorker message = Mockito.mock(HeartbeatFromWorker.class);
+
+    HeartbeatFromWorkerResponse response = null;
+    try {
+      response = client.askSync(message, HeartbeatFromWorkerResponse.class);
+    } catch (Throwable t) {
+      LOG.error("It should redirect to the suggested leader during 
bootstrap.", t);
+      fail("It should redirect to the suggested leader during bootstrap.");
+    }
+
+    assertEquals(mockResponse, response);
+  }
+
+  @Test
+  public void 
testBootstrapMasterNotLeaderWithoutLeaderContinuesToOtherConfiguredMastersInHA()
 {
+    final CelebornConf conf = prepareForCelebornConfWithHA();
+
+    final RpcEndpointRef master2 = Mockito.mock(RpcEndpointRef.class);
+    Mockito.doReturn(Future$.MODULE$.successful(mockResponse))
+        .when(master2)
+        .ask(Mockito.any(), Mockito.any(), Mockito.any());
+
+    Mockito.doAnswer(
+            invocation -> {
+              RpcAddress address = invocation.getArgument(0, RpcAddress.class);
+              switch (address.host()) {
+                case "host1":
+                  throw new RuntimeException(
+                      new MasterNotLeaderException(
+                          "host1:9097", 
MasterNotLeaderException.LEADER_NOT_PRESENTED, null));
+                case "host2":
+                  return master2;
+                default:
+                  fail("Should use master host1/host2:" + masterPort + ", but 
use " + address);
+              }
+              return null;
+            })
+        .when(rpcEnv)
+        .setupEndpointRef(Mockito.any(RpcAddress.class), Mockito.anyString());
+
+    MasterClient client = new MasterClient(rpcEnv, conf, false);
+    HeartbeatFromWorker message = Mockito.mock(HeartbeatFromWorker.class);
+
+    HeartbeatFromWorkerResponse response = null;
+    try {
+      response = client.askSync(message, HeartbeatFromWorkerResponse.class);
+    } catch (Throwable t) {
+      LOG.error("It should continue to another configured master during 
bootstrap.", t);
+      fail("It should continue to another configured master during 
bootstrap.");
+    }
+
+    assertEquals(mockResponse, response);
+  }
+
+  @Test
+  public void testSuggestedLeaderSetupFailureRetriesAnotherMasterInHA() {
+    final CelebornConf conf =
+        conf()
+            .set(CelebornConf.HA_ENABLED().key(), "true")
+            .set(CelebornConf.MASTER_ENDPOINTS().key(), 
"host1:9097,host4:9097")
+            .set(CelebornConf.MASTER_CLIENT_MAX_RETRIES().key(), "5");
+
+    final RpcEndpointRef master1 = Mockito.mock(RpcEndpointRef.class);
+    final RpcEndpointRef master3 = Mockito.mock(RpcEndpointRef.class);
+
+    Mockito.doReturn(
+            Future$.MODULE$.failed(new MasterNotLeaderException("host1:9097", 
"host2:9097", null)))
+        .when(master1)
+        .ask(Mockito.any(), Mockito.any(), Mockito.any());
+    Mockito.doReturn(Future$.MODULE$.successful(mockResponse))
+        .when(master3)
+        .ask(Mockito.any(), Mockito.any(), Mockito.any());
+
+    Mockito.doAnswer(
+            invocation -> {
+              RpcAddress address = invocation.getArgument(0, RpcAddress.class);
+              switch (address.host()) {
+                case "host1":
+                  return master1;
+                case "host2":
+                  throw new RuntimeException(
+                      new MasterNotLeaderException("host2:9097", "host3:9097", 
null));
+                case "host3":
+                  return master3;
+                case "host4":
+                  throw new AssertionError("Should follow chained suggested 
leaders before host4.");
+                default:
+                  fail(
+                      "Should use master host1/host2/host3:" + masterPort + ", 
but use " + address);
+              }
+              return null;
+            })
+        .when(rpcEnv)
+        .setupEndpointRef(Mockito.any(RpcAddress.class), Mockito.anyString());
+
+    MasterClient client = new MasterClient(rpcEnv, conf, false);
+    HeartbeatFromWorker message = Mockito.mock(HeartbeatFromWorker.class);
+
+    HeartbeatFromWorkerResponse response = null;
+    try {
+      response = client.askSync(message, HeartbeatFromWorkerResponse.class);
+    } catch (Throwable t) {
+      LOG.error("It should retry with another master when suggested leader 
setup races.", t);
+      fail("It should retry with another master when suggested leader setup 
races.");
+    }
+
+    assertEquals(mockResponse, response);
+  }
+
+  @Test
+  public void testSuggestedLeaderRedirectCycleRetriesConfiguredMasterInHA() {
+    final CelebornConf conf =
+        conf()
+            .set(CelebornConf.HA_ENABLED().key(), "true")
+            .set(CelebornConf.MASTER_ENDPOINTS().key(), 
"host1:9097,host4:9097")
+            .set(CelebornConf.MASTER_CLIENT_MAX_RETRIES().key(), "5");
+
+    final RpcEndpointRef master1 = Mockito.mock(RpcEndpointRef.class);
+    final RpcEndpointRef master4 = Mockito.mock(RpcEndpointRef.class);
+
+    Mockito.doReturn(
+            Future$.MODULE$.failed(new MasterNotLeaderException("host1:9097", 
"host2:9097", null)))
+        .when(master1)
+        .ask(Mockito.any(), Mockito.any(), Mockito.any());
+    Mockito.doReturn(Future$.MODULE$.successful(mockResponse))
+        .when(master4)
+        .ask(Mockito.any(), Mockito.any(), Mockito.any());
+
+    Mockito.doAnswer(
+            invocation -> {
+              RpcAddress address = invocation.getArgument(0, RpcAddress.class);
+              switch (address.host()) {
+                case "host1":
+                  return master1;
+                case "host2":
+                  throw new RuntimeException(
+                      new MasterNotLeaderException("host2:9097", "host3:9097", 
null));
+                case "host3":
+                  throw new RuntimeException(
+                      new MasterNotLeaderException("host3:9097", "host2:9097", 
null));
+                case "host4":
+                  return master4;
+                default:
+                  fail(
+                      "Should use master host1/host2/host3/host4:"
+                          + masterPort
+                          + ", but use "
+                          + address);
+              }
+              return null;
+            })
+        .when(rpcEnv)
+        .setupEndpointRef(Mockito.any(RpcAddress.class), Mockito.anyString());
+
+    MasterClient client = new MasterClient(rpcEnv, conf, false);
+    HeartbeatFromWorker message = Mockito.mock(HeartbeatFromWorker.class);
+
+    HeartbeatFromWorkerResponse response = null;
+    try {
+      response = client.askSync(message, HeartbeatFromWorkerResponse.class);
+    } catch (Throwable t) {
+      LOG.error("It should retry another configured master after a redirect 
cycle.", t);
+      fail("It should retry another configured master after a redirect 
cycle.");
+    }
+
+    assertEquals(mockResponse, response);
+    Mockito.verify(rpcEnv, Mockito.times(1))
+        .setupEndpointRef(
+            Mockito.eq(RpcAddress.fromHostAndPort("host2:9097")), 
Mockito.anyString());
+    Mockito.verify(rpcEnv, Mockito.times(1))
+        .setupEndpointRef(
+            Mockito.eq(RpcAddress.fromHostAndPort("host3:9097")), 
Mockito.anyString());
+  }
+
+  @Test
+  public void 
testBootstrapSuggestedLeaderConnectionFailureRetriesConfiguredMasterInHA() {
+    final CelebornConf conf = prepareForCelebornConfWithHA();
+
+    final RpcEndpointRef master3 = Mockito.mock(RpcEndpointRef.class);
+
+    Mockito.doReturn(Future$.MODULE$.successful(mockResponse))
+        .when(master3)
+        .ask(Mockito.any(), Mockito.any(), Mockito.any());
+
+    Mockito.doAnswer(
+            invocation -> {
+              RpcAddress address = invocation.getArgument(0, RpcAddress.class);
+              switch (address.host()) {
+                case "host1":
+                  throw new RuntimeException(
+                      new MasterNotLeaderException("host1:9097", "host2:9097", 
null));
+                case "host2":
+                  throw new IOException("test");
+                case "host3":
+                  return master3;
+                default:
+                  fail(
+                      "Should use master host1/host2/host3:" + masterPort + ", 
but use " + address);
+              }
+              return null;
+            })
+        .when(rpcEnv)
+        .setupEndpointRef(Mockito.any(RpcAddress.class), Mockito.anyString());
+
+    MasterClient client = new MasterClient(rpcEnv, conf, false);
+    HeartbeatFromWorker message = Mockito.mock(HeartbeatFromWorker.class);
+
+    HeartbeatFromWorkerResponse response = null;
+    try {
+      response = client.askSync(message, HeartbeatFromWorkerResponse.class);
+    } catch (Throwable t) {
+      LOG.error("It should retry another configured master after bootstrap 
redirect fails.", t);
+      fail("It should retry another configured master after bootstrap redirect 
fails.");
+    }
+
+    assertEquals(mockResponse, response);
+  }
+
   private void checkOneMasterDownInHA(Exception causedByException) {
     final CelebornConf conf = prepareForCelebornConfWithHA();
 
@@ -440,15 +691,19 @@ public class MasterClientSuiteJ {
   }
 
   private CelebornConf prepareForCelebornConfWithoutHA() {
-    return conf.clone()
+    return conf()
         .set(CelebornConf.HA_ENABLED().key(), "false")
         .set(CelebornConf.MASTER_ENDPOINTS().key(), masterHost + ":" + 
masterPort);
   }
 
   private CelebornConf prepareForCelebornConfWithHA() {
-    return conf.clone()
+    return conf()
         .set(CelebornConf.HA_ENABLED().key(), "true")
         .set(CelebornConf.MASTER_ENDPOINTS().key(), 
"host1:9097,host2:9097,host3:9097")
         .set(CelebornConf.MASTER_CLIENT_MAX_RETRIES().key(), "5");
   }
+
+  private CelebornConf conf() {
+    return conf.clone();
+  }
 }

Reply via email to