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


##########
common/src/main/java/org/apache/celeborn/common/network/client/TransportClient.java:
##########
@@ -92,7 +92,15 @@ public Channel getChannel() {
   }
 
   public boolean isActive() {
-    return !timedOut && (channel.isOpen() || channel.isActive());
+    // A channel is pinned to one netty event-loop thread for its lifetime. If 
that event loop has
+    // terminated (e.g. an uncaught error killed the thread; netty does not 
replace it in a
+    // fixed-size EventLoopGroup), the channel can no longer send or complete 
anything: writes and
+    // listener notifications route to the dead loop and are silently dropped. 
Such a client must
+    // not be treated as active/reused, otherwise a request on it can orphan 
and hang. See
+    // SPARK-58292.
+    return !timedOut
+        && !channel.eventLoop().isShuttingDown()

Review Comment:
   You're right, and thanks for tracing it to `CelebornBufferStream` 
specifically -- that is exactly the case that eviction alone does not cover, 
since it holds the client for the lifetime of the partition and never 
reacquires it.
   
   Two things now fail the work instead of letting it orphan:
   
   - `TransportClientFactory.failClientsOnDeadEventLoops()` -- once a 
connection failure has revealed a dead loop, it synchronously fails the 
outstanding requests of every pooled client still pinned to one. It 
deliberately runs **outside** the connection-pool lock (from a `finally` in 
`createClient`), because failing a request invokes its callback on the calling 
thread and callbacks re-enter the factory; doing it under `clientPool.locks[i]` 
would invert the pool-then-factory lock order.
   - `TransportClient.sendRpc()` fails the callback up front rather than 
writing into a dead loop, which covers credits issued *after* the sweep. This 
matters most for RPCs specifically: 
`failExpiredPushRequest`/`failExpiredFetchRequest` run on their own checker 
threads and cover `outstandingPushes`/`outstandingFetches`, but 
`outstandingRpcs` has no such backstop -- which is why the Flink credit path 
was the one that hung indefinitely.
   
   On force-closing: I could not make that work, and I think your own repro 
says why -- `channel.close()` is itself submitted to the dead loop, so it never 
takes effect. Failing the callbacks is the strongest available substitute, and 
it is what lets the stream owner observe the failure: `onFailure` -> 
`messageConsumer.accept(new TransportableError(...))` -> the reader tears down 
and reopens.
   
   Two related notes:
   
   - `closeStream()` skipping `BUFFER_STREAM_END` is not a regression from the 
`isActive()` change. Before it, the `sendRpc` was enqueued to the dead loop and 
never executed, so the server did not receive it either way.
   - While tracing this I found `CelebornBufferStream` logged `e.getCause()` 
(null for these failures) in `addCredit`/`notifyRequiredSegment` and dropped 
the throwable entirely in `openStreamInternal`, so the recovery was 
undiagnosable in exactly this scenario. Fixed in the same commit.
   
   Covered by 
`TransportClientFactorySuiteJ.failsOutstandingRequestsOfPooledClientsOnDeadEventLoops`
 and 
`TransportClientSuiteJ.{sendRpcFailsFastWhenEventLoopIsDead,invalidatesClientWhenEventLoopIsDead}`.



-- 
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