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 17159eb34a [CELEBORN-2369] Retry stopped master Outbox failures
17159eb34a is described below

commit 17159eb34ac8132e53e8f19b2c8d38d63d7853cb
Author: Chao Sun <[email protected]>
AuthorDate: Tue Jun 30 11:23:05 2026 +0800

    [CELEBORN-2369] Retry stopped master Outbox failures
    
    ## Why are the changes needed?
    
    When a Celeborn client loses its cached master connection, the associated 
Outbox can be stopped before queued or late master RPC messages are sent. Those 
requests fail with `CelebornException: Message is dropped because Outbox is 
stopped`.
    
    `MasterClient` currently retries I/O and RPC timeout failures, but it does 
not recognize this stopped-Outbox failure when it is wrapped by the normal 
`awaitResult` path. In HA mode, a request can therefore fail instead of 
clearing the stale `RpcEndpointRef` and reconnecting to another available 
master.
    
    The same legacy message is also used when the local `RpcEnv` shuts down 
permanently. Treating every occurrence as retryable without distinguishing 
shutdown would cause futile reconnect attempts through an already-stopped 
environment.
    
    JIRA: https://issues.apache.org/jira/browse/CELEBORN-2369
    
    ## What changes were proposed in this PR?
    
    - Recognize the exact legacy stopped-Outbox failure through its cause chain 
and reset the cached master endpoint so the existing HA retry path can 
reconnect.
    - Use `RpcEnvStoppedException` for terminal local shutdown so shutdown 
failures remain non-retryable.
    - Preserve each Outbox's original stop cause for queued and late messages.
    - Fail messages submitted after `RpcEnv` shutdown immediately with the 
terminal cause.
    - Add focused coverage for HA reconnection, terminal no-reconnect behavior, 
and transient versus terminal Outbox causes.
    
    ### Does this PR resolve a correctness bug?
    
    - [x] Yes
    
    ### Does this PR introduce _any_ user-facing change?
    
    - [x] Yes. Master RPCs can now fail over after a transient stopped-Outbox 
failure instead of failing immediately. No public API or configuration changes 
are introduced.
    
    ## How was this PR tested?
    
    Using JDK 17:
    
    - `build/mvn --no-transfer-progress -DskipTests spotless:apply`
    - `build/mvn --no-transfer-progress -pl common -DskipTests test-compile`
    - `build/mvn --no-transfer-progress -pl common -DargLine= 
-Dtest=org.apache.celeborn.common.client.MasterClientSuiteJ surefire:test` — 17 
tests passed.
    - `build/mvn --no-transfer-progress -pl common -DargLine= 
-DwildcardSuites=org.apache.celeborn.common.rpc.netty.OutboxSuite 
scalatest:test` — 3 tests passed.
    - `build/mvn --no-transfer-progress -pl common -DargLine= 
-DwildcardSuites=org.apache.celeborn.common.rpc.netty.NettyRpcEnvSuite 
scalatest:test` — 33 tests passed.
    - `build/mvn --no-transfer-progress -DskipTests spotless:check` — all 10 
reactor modules passed.
    
    Closes #3744 from sunchao/codex/retry-stopped-master-outbox-oss.
    
    Authored-by: Chao Sun <[email protected]>
    Signed-off-by: Nicholas Jiang <[email protected]>
---
 .../celeborn/common/client/MasterClient.java       |  16 ++-
 .../common/rpc/OutboxStoppedException.scala        |  27 +++++
 .../celeborn/common/rpc/netty/NettyRpcEnv.scala    |   8 +-
 .../apache/celeborn/common/rpc/netty/Outbox.scala  |  36 ++++---
 .../celeborn/common/client/MasterClientSuiteJ.java | 114 +++++++++++++++++++++
 .../common/rpc/netty/NettyRpcEnvSuite.scala        |  29 ++++++
 .../celeborn/common/rpc/netty/OutboxSuite.scala    |  81 +++++++++++++++
 7 files changed, 294 insertions(+), 17 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 e57460c49c..252ad018a5 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
@@ -186,13 +186,27 @@ public class MasterClient {
         resetRpcEndpointRef(oldRef);
       }
       return true;
-    } else if (e.getCause() instanceof IOException || e instanceof 
RpcTimeoutException) {
+    } else if (isRetryableRpcFailure(e)) {
       resetRpcEndpointRef(oldRef);
       return true;
     }
     return false;
   }
 
+  private boolean isRetryableRpcFailure(Throwable throwable) {
+    if (throwable.getCause() instanceof IOException || throwable instanceof 
RpcTimeoutException) {
+      return true;
+    }
+    Throwable current = throwable;
+    while (current != null) {
+      if (current instanceof OutboxStoppedException) {
+        return true;
+      }
+      current = current.getCause();
+    }
+    return false;
+  }
+
   @Nullable
   private MasterNotLeaderException findMasterNotLeaderException(Throwable 
throwable) {
     Throwable current = throwable;
diff --git 
a/common/src/main/scala/org/apache/celeborn/common/rpc/OutboxStoppedException.scala
 
b/common/src/main/scala/org/apache/celeborn/common/rpc/OutboxStoppedException.scala
new file mode 100644
index 0000000000..14e97ad8a6
--- /dev/null
+++ 
b/common/src/main/scala/org/apache/celeborn/common/rpc/OutboxStoppedException.scala
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+package org.apache.celeborn.common.rpc
+
+import org.apache.celeborn.common.exception.CelebornException
+
+private[celeborn] class OutboxStoppedException()
+  extends CelebornException(OutboxStoppedException.MESSAGE)
+
+private[celeborn] object OutboxStoppedException {
+  val MESSAGE = "Message is dropped because Outbox is stopped"
+}
diff --git 
a/common/src/main/scala/org/apache/celeborn/common/rpc/netty/NettyRpcEnv.scala 
b/common/src/main/scala/org/apache/celeborn/common/rpc/netty/NettyRpcEnv.scala
index ec72194916..ed002d3b0b 100644
--- 
a/common/src/main/scala/org/apache/celeborn/common/rpc/netty/NettyRpcEnv.scala
+++ 
b/common/src/main/scala/org/apache/celeborn/common/rpc/netty/NettyRpcEnv.scala
@@ -105,6 +105,8 @@ class NettyRpcEnv(
 
   private val stopped = new AtomicBoolean(false)
 
+  private[netty] def isStopped: Boolean = stopped.get()
+
   /**
    * A map for [[RpcAddress]] and [[Outbox]]. When we are connecting to a 
remote [[RpcAddress]],
    * we just put messages to its [[Outbox]] to implement a non-blocking `send` 
method.
@@ -208,7 +210,9 @@ class NettyRpcEnv(
       if (stopped.get) {
         // It's possible that we put `targetOutbox` after stopping. So we need 
to clean it.
         outboxes.remove(receiver.address)
-        targetOutbox.stop()
+        val cause = new RpcEnvStoppedException()
+        targetOutbox.stop(cause)
+        message.onFailure(cause)
       } else {
         targetOutbox.send(message)
       }
@@ -336,7 +340,7 @@ class NettyRpcEnv(
     while (iter.hasNext()) {
       val outbox = iter.next()
       outboxes.remove(outbox.address)
-      outbox.stop()
+      outbox.stop(new RpcEnvStoppedException())
     }
     if (timeoutScheduler != null) {
       timeoutScheduler.shutdownNow()
diff --git 
a/common/src/main/scala/org/apache/celeborn/common/rpc/netty/Outbox.scala 
b/common/src/main/scala/org/apache/celeborn/common/rpc/netty/Outbox.scala
index 948278b887..52b8e481f8 100644
--- a/common/src/main/scala/org/apache/celeborn/common/rpc/netty/Outbox.scala
+++ b/common/src/main/scala/org/apache/celeborn/common/rpc/netty/Outbox.scala
@@ -23,10 +23,9 @@ import javax.annotation.concurrent.GuardedBy
 
 import scala.util.control.NonFatal
 
-import org.apache.celeborn.common.exception.CelebornException
 import org.apache.celeborn.common.internal.Logging
 import org.apache.celeborn.common.network.client.{RpcResponseCallback, 
TransportClient}
-import org.apache.celeborn.common.rpc.{RpcAddress, RpcEnvStoppedException}
+import org.apache.celeborn.common.rpc.{OutboxStoppedException, RpcAddress, 
RpcEnvStoppedException}
 
 sealed private[celeborn] trait OutboxMessage {
 
@@ -104,6 +103,9 @@ private[celeborn] class Outbox(nettyEnv: NettyRpcEnv, val 
address: RpcAddress) {
   @GuardedBy("this")
   private var stopped = false
 
+  @GuardedBy("this")
+  private var stopCause: Throwable = null
+
   /**
    * If there is any thread draining the message queue
    */
@@ -112,19 +114,20 @@ private[celeborn] class Outbox(nettyEnv: NettyRpcEnv, val 
address: RpcAddress) {
 
   /**
    * Send a message. If there is no active connection, cache it and launch a 
new connection. If
-   * [[Outbox]] is stopped, the sender will be notified with a 
[[CelebornException]].
+   * [[Outbox]] is stopped, the sender will be notified with the cause that 
stopped it.
    */
   def send(message: OutboxMessage): Unit = {
-    val dropped = synchronized {
+    val failure = synchronized {
       if (stopped) {
-        true
+        assert(stopCause != null)
+        stopCause
       } else {
         messages.add(message)
-        false
+        null
       }
     }
-    if (dropped) {
-      message.onFailure(new CelebornException("Message is dropped because 
Outbox is stopped"))
+    if (failure != null) {
+      message.onFailure(failure)
     } else {
       drainOutbox()
     }
@@ -225,6 +228,7 @@ private[celeborn] class Outbox(nettyEnv: NettyRpcEnv, val 
address: RpcAddress) {
         return
       }
       stopped = true
+      stopCause = e
       closeClient()
     }
     // Remove this Outbox from nettyEnv so that the further messages will 
create a new Outbox along
@@ -248,16 +252,20 @@ private[celeborn] class Outbox(nettyEnv: NettyRpcEnv, val 
address: RpcAddress) {
     client = null
   }
 
-  /**
-   * Stop [[Outbox]]. The remaining messages in the [[Outbox]] will be 
notified with a
-   * [[CelebornException]].
-   */
-  def stop(): Unit = {
+  /** Stop [[Outbox]] using a terminal cause when its owning RPC environment 
is shutting down. */
+  def stop(): Unit =
+    stop(
+      if (nettyEnv.isStopped) new RpcEnvStoppedException()
+      else new OutboxStoppedException())
+
+  /** Stop [[Outbox]] and notify the remaining messages with the supplied 
cause. */
+  def stop(cause: Throwable): Unit = {
     synchronized {
       if (stopped) {
         return
       }
       stopped = true
+      stopCause = cause
       if (connectFuture != null) {
         connectFuture.cancel(true)
       }
@@ -268,7 +276,7 @@ private[celeborn] class Outbox(nettyEnv: NettyRpcEnv, val 
address: RpcAddress) {
     // update messages and it's safe to just drain the queue.
     var message = messages.poll()
     while (message != null) {
-      message.onFailure(new CelebornException("Message is dropped because 
Outbox is stopped"))
+      message.onFailure(cause)
       message = messages.poll()
     }
   }
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 26ad11c432..e9781a9f4a 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
@@ -18,6 +18,7 @@
 package org.apache.celeborn.common.client;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -44,9 +45,11 @@ import 
org.apache.celeborn.common.protocol.message.ControlMessages.HeartbeatFrom
 import 
org.apache.celeborn.common.protocol.message.ControlMessages.HeartbeatFromWorker;
 import 
org.apache.celeborn.common.protocol.message.ControlMessages.HeartbeatFromWorkerResponse;
 import 
org.apache.celeborn.common.protocol.message.ControlMessages.OneWayMessageResponse$;
+import org.apache.celeborn.common.rpc.OutboxStoppedException;
 import org.apache.celeborn.common.rpc.RpcAddress;
 import org.apache.celeborn.common.rpc.RpcEndpointRef;
 import org.apache.celeborn.common.rpc.RpcEnv;
+import org.apache.celeborn.common.rpc.RpcEnvStoppedException;
 import org.apache.celeborn.common.rpc.RpcTimeoutException;
 
 public class MasterClientSuiteJ {
@@ -245,6 +248,84 @@ public class MasterClientSuiteJ {
     checkOneMasterAskFailedInHA(new RpcTimeoutException("test", new 
TimeoutException("test")));
   }
 
+  @Test
+  public void testStoppedOutboxFailureReconnectsToAnotherMasterInHA() {
+    final CelebornConf conf = prepareForCelebornConfWithHA();
+
+    final RpcEndpointRef master1 = Mockito.mock(RpcEndpointRef.class);
+    final RpcEndpointRef master2 = Mockito.mock(RpcEndpointRef.class);
+    final AtomicInteger master1Attempts = new AtomicInteger(0);
+
+    Mockito.doAnswer(
+            invocation -> {
+              assertEquals(0, master1Attempts.getAndIncrement());
+              return Future$.MODULE$.failed(new OutboxStoppedException());
+            })
+        .when(master1)
+        .ask(Mockito.any(), Mockito.any(), Mockito.any());
+    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":
+                  return master1;
+                case "host2":
+                  return master2;
+                default:
+                  fail(
+                      "Should reconnect from host1 to 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 reconnect after a stopped outbox failure.", t);
+      fail("It should reconnect after a stopped outbox failure.");
+    }
+
+    assertEquals(mockResponse, response);
+    assertEquals(1, master1Attempts.get());
+    Mockito.verify(rpcEnv, Mockito.times(1))
+        .setupEndpointRef(
+            Mockito.eq(RpcAddress.fromHostAndPort("host1:9097")), 
Mockito.anyString());
+    Mockito.verify(rpcEnv, Mockito.times(1))
+        .setupEndpointRef(
+            Mockito.eq(RpcAddress.fromHostAndPort("host2:9097")), 
Mockito.anyString());
+  }
+
+  @Test
+  public void testStoppedRpcEnvFailureDoesNotReconnectInHA() {
+    checkMasterAskFailureDoesNotReconnectInHA(new RpcEnvStoppedException());
+  }
+
+  @Test
+  public void testNestedIOExceptionDoesNotReconnectInHA() {
+    checkMasterAskFailureDoesNotReconnectInHA(
+        new CelebornException("Permanent failure", new IOException("test")));
+  }
+
+  @Test
+  public void testNestedRpcTimeoutDoesNotReconnectInHA() {
+    checkMasterAskFailureDoesNotReconnectInHA(
+        new CelebornException(
+            "Permanent failure", new RpcTimeoutException("test", new 
TimeoutException("test"))));
+  }
+
   @Test
   public void testBootstrapMasterNotLeaderRedirectsToSuggestedLeaderInHA() {
     final CelebornConf conf =
@@ -545,6 +626,39 @@ public class MasterClientSuiteJ {
     assertEquals(mockResponse, response);
   }
 
+  private void checkMasterAskFailureDoesNotReconnectInHA(Exception exception) {
+    final CelebornConf conf = prepareForCelebornConfWithHA();
+    final RpcEndpointRef master1 = Mockito.mock(RpcEndpointRef.class);
+
+    Mockito.doReturn(Future$.MODULE$.failed(exception))
+        .when(master1)
+        .ask(Mockito.any(), Mockito.any(), Mockito.any());
+    Mockito.doAnswer(
+            invocation -> {
+              RpcAddress address = invocation.getArgument(0, RpcAddress.class);
+              if ("host1".equals(address.host())) {
+                return master1;
+              }
+              fail("Should not reconnect after a non-retryable failure: " + 
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);
+
+    CelebornException thrown =
+        assertThrows(
+            CelebornException.class,
+            () -> client.askSync(message, HeartbeatFromWorkerResponse.class));
+    assertSame(exception, thrown.getCause());
+    Mockito.verify(master1, Mockito.times(1)).ask(Mockito.any(), 
Mockito.any(), Mockito.any());
+    Mockito.verify(rpcEnv, Mockito.times(1))
+        .setupEndpointRef(
+            Mockito.eq(RpcAddress.fromHostAndPort("host1:9097")), 
Mockito.anyString());
+  }
+
   private void checkOneMasterAskFailedInHA(Exception exception) {
     final CelebornConf conf = prepareForCelebornConfWithHA();
 
diff --git 
a/common/src/test/scala/org/apache/celeborn/common/rpc/netty/NettyRpcEnvSuite.scala
 
b/common/src/test/scala/org/apache/celeborn/common/rpc/netty/NettyRpcEnvSuite.scala
index f42ffab4cf..3cdb3fcfe5 100644
--- 
a/common/src/test/scala/org/apache/celeborn/common/rpc/netty/NettyRpcEnvSuite.scala
+++ 
b/common/src/test/scala/org/apache/celeborn/common/rpc/netty/NettyRpcEnvSuite.scala
@@ -65,6 +65,35 @@ class NettyRpcEnvSuite extends RpcEnvSuite with TimeLimits {
     assert(e.getCause.getMessage.contains(uri))
   }
 
+  test("ask through a stopped RPC environment fails immediately") {
+    val endpointName = "stopped-rpc-env"
+    env.setupEndpoint(
+      endpointName,
+      new RpcEndpoint {
+        override val rpcEnv: RpcEnv = env
+        override def receiveAndReply(context: RpcCallContext): 
PartialFunction[Any, Unit] = {
+          case message => context.reply(message)
+        }
+      })
+    val clientEnv = createRpcEnv(createCelebornConf(), "stopped-client", 0, 
clientMode = true)
+    try {
+      val endpointRef = clientEnv.setupEndpointRef(env.address, endpointName)
+
+      clientEnv.shutdown()
+      clientEnv.awaitTermination()
+
+      failAfter(5.seconds) {
+        val e = intercept[CelebornException] {
+          endpointRef.askSync[String]("hello")
+        }
+        assert(e.getCause.isInstanceOf[RpcEnvStoppedException])
+      }
+    } finally {
+      clientEnv.shutdown()
+      clientEnv.awaitTermination()
+    }
+  }
+
   test("advertise address different from bind address") {
     val celebornConf = createCelebornConf()
     val config = RpcEnvConfig(
diff --git 
a/common/src/test/scala/org/apache/celeborn/common/rpc/netty/OutboxSuite.scala 
b/common/src/test/scala/org/apache/celeborn/common/rpc/netty/OutboxSuite.scala
new file mode 100644
index 0000000000..ff37f50cf0
--- /dev/null
+++ 
b/common/src/test/scala/org/apache/celeborn/common/rpc/netty/OutboxSuite.scala
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+package org.apache.celeborn.common.rpc.netty
+
+import java.nio.ByteBuffer
+import java.util.concurrent.{CountDownLatch, TimeUnit}
+import java.util.concurrent.atomic.AtomicReference
+
+import org.mockito.Mockito.{mock, when}
+
+import org.apache.celeborn.CelebornFunSuite
+import org.apache.celeborn.common.rpc.{OutboxStoppedException, RpcAddress, 
RpcEnvStoppedException}
+
+class OutboxSuite extends CelebornFunSuite {
+
+  private def failureMessage(
+      failure: AtomicReference[Throwable],
+      failed: CountDownLatch): RpcOutboxMessage =
+    RpcOutboxMessage(
+      ByteBuffer.allocate(0),
+      e => {
+        failure.set(e)
+        failed.countDown()
+      },
+      (_, _) => ())
+
+  test("send after terminal stop uses the original cause") {
+    val outbox = new Outbox(mock(classOf[NettyRpcEnv]), 
RpcAddress("localhost", 12345))
+    val cause = new RpcEnvStoppedException()
+    val failure = new AtomicReference[Throwable]()
+    val failed = new CountDownLatch(1)
+
+    outbox.stop(cause)
+    outbox.send(failureMessage(failure, failed))
+
+    assert(failed.await(10, TimeUnit.SECONDS))
+    assert(failure.get() eq cause)
+  }
+
+  test("send after transient stop remains retryable") {
+    val outbox = new Outbox(mock(classOf[NettyRpcEnv]), 
RpcAddress("localhost", 12345))
+    val failure = new AtomicReference[Throwable]()
+    val failed = new CountDownLatch(1)
+
+    outbox.stop()
+    outbox.send(failureMessage(failure, failed))
+
+    assert(failed.await(10, TimeUnit.SECONDS))
+    assert(failure.get().isInstanceOf[OutboxStoppedException])
+    assert(failure.get().getMessage === OutboxStoppedException.MESSAGE)
+  }
+
+  test("default stop after RPC environment shutdown uses the terminal cause") {
+    val nettyEnv = mock(classOf[NettyRpcEnv])
+    val outbox = new Outbox(nettyEnv, RpcAddress("localhost", 12345))
+    val failure = new AtomicReference[Throwable]()
+    val failed = new CountDownLatch(1)
+    when(nettyEnv.isStopped).thenReturn(true)
+
+    outbox.stop()
+    outbox.send(failureMessage(failure, failed))
+
+    assert(failed.await(10, TimeUnit.SECONDS))
+    assert(failure.get().isInstanceOf[RpcEnvStoppedException])
+  }
+}

Reply via email to