SteNicholas commented on code in PR #3744:
URL: https://github.com/apache/celeborn/pull/3744#discussion_r3472022435


##########
common/src/main/java/org/apache/celeborn/common/client/MasterClient.java:
##########
@@ -186,13 +189,28 @@ private boolean shouldRetry(@Nullable RpcEndpointRef 
oldRef, Throwable e) {
         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) {
+    Throwable current = throwable;
+    while (current != null) {
+      if (current instanceof IOException || current instanceof 
RpcTimeoutException) {

Review Comment:
   This rewrite also changes the pre-existing `IOException` / 
`RpcTimeoutException` classification, which I don't think is the PR's intent.
   
   Previously the check was `e.getCause() instanceof IOException || e 
instanceof RpcTimeoutException` — `IOException` was matched **only one level 
down** (at `e.getCause()`) and `RpcTimeoutException` **only at the top**. 
Walking the full cause chain now makes either type retryable **at any depth, 
including the top level**. So a top-level `IOException`, or one wrapped 2+ 
levels deep inside an otherwise-permanent failure, now triggers 
`resetRpcEndpointRef` + full HA failover/`maxRetries` where it previously 
surfaced immediately.
   
   If broadening I/O retry is intended, could you note it in the PR 
description? If not, consider keeping the original 
`IOException`/`RpcTimeoutException` matching and only adding the new 
stopped-Outbox `CelebornException` arm to the walk.



##########
common/src/main/java/org/apache/celeborn/common/client/MasterClient.java:
##########
@@ -74,6 +75,8 @@ public MasterClient(RpcEnv rpcEnv, CelebornConf conf, boolean 
isWorker) {
   }
 
   private static final String SPLITTER = "#";
+  private static final String OUTBOX_STOPPED_MESSAGE =

Review Comment:
   The retry contract now hinges on this exact string, which is independently 
re-declared in `Outbox.scala` (`stop()` -> `new CelebornException("Message is 
dropped because Outbox is stopped")`, ~line 260). The two copies are coupled 
only by byte-for-byte equality across a module/language boundary — no shared 
constant, no test asserting they match.
   
   A reword, typo-fix, prefix, or i18n on either side would silently make this 
matcher stop recognizing the failure, regressing HA failover back to the exact 
bug this PR fixes, with no compile-time or test signal.
   
   Suggest a dedicated marker exception type (e.g. `OutboxStoppedException 
extends CelebornException`) matched via `instanceof`, so producer and consumer 
share one source of truth instead of a string literal.



##########
common/src/main/scala/org/apache/celeborn/common/rpc/netty/Outbox.scala:
##########
@@ -248,16 +253,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 =

Review Comment:
   The no-arg `stop()` derives opposite-retryability causes 
(`RpcEnvStoppedException` = terminal vs. the `CelebornException` = retryable) 
from a live read of `nettyEnv.isStopped`, outside the `synchronized` block in 
`stop(cause)`. The only remaining no-arg caller is `removeOutbox` (from 
`channelInactive` on a remote disconnect), so the retryability of those drained 
messages depends on a global mutable flag rather than the caller's own intent.
   
   It's safe today because the env-shutdown path that observes `isStopped == 
true` is the intended-terminal case, but routing the decision through a global 
flag is fragile — consider having `removeOutbox` pass an explicit cause 
instead. (A marker exception type, per the `MasterClient` comment, would also 
remove the magic string from this branch.)



##########
common/src/test/scala/org/apache/celeborn/common/rpc/netty/OutboxSuite.scala:
##########
@@ -0,0 +1,82 @@
+/*
+ * 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.exception.CelebornException
+import org.apache.celeborn.common.rpc.{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))

Review Comment:
   `isStopped` is never stubbed on this mock, so the assertion that the failure 
is the retryable `CelebornException` relies on Mockito's default `false` 
return. If `isStopped` is later refactored or the mock made strict, `stop()` 
would silently take the `RpcEnvStoppedException` branch and this test would 
pass against the wrong (non-retryable) cause. Recommend stubbing 
`when(...isStopped).thenReturn(false)` explicitly to pin the intended path.



##########
common/src/test/scala/org/apache/celeborn/common/rpc/netty/NettyRpcEnvSuite.scala:
##########
@@ -65,6 +65,30 @@ 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)

Review Comment:
   Unlike the other env-creating tests in this suite (e.g. lines 106-109, 
195-202), `clientEnv` here isn't wrapped in `try { ... } finally { 
clientEnv.shutdown() }`. If `setupEndpointRef` throws (e.g. a bind/connect 
race), the env is never shut down and its Netty event-loop threads + timeout 
scheduler leak into subsequent tests (flaky-CI risk). Suggest the same 
try/finally pattern used elsewhere in the suite.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to