ChuckLin2025 opened a new pull request, #57462:
URL: https://github.com/apache/spark/pull/57462

   ### What changes were proposed in this pull request?
   
   A netty worker event-loop thread that dies (a `Throwable` escaping `run()` 
at the `runIo()`/select level; per-task exceptions are swallowed by 
`safeExecute`) is driven to `ST_TERMINATED` by 
`SingleThreadEventExecutor.doStartThread()`'s finally block. Once that happens 
the thread is:
   
   - **never replaced** in the fixed-size `MultithreadEventExecutorGroup` 
(`children` is final, there is no repopulation),
   - still **handed out** by the round-robin `EventExecutorChooser`, which has 
no liveness check, and
   - **not restartable** (`startThread()` only starts from 
`ST_NOT_STARTED`/`ST_SUSPENDED`, never from `ST_TERMINATED`).
   
   So the dead loop permanently poisons any channel pinned to it, which 
surfaces in `TransportClientFactory` as two failure modes:
   
   1. **New connections (~1/N fail):** a fresh channel bound to the dead loop 
fails registration with `RejectedExecutionException("event executor 
terminated")` (caught by `AbstractChannel.AbstractUnsafe.register`), so 
`createClient` throws `IOException`. Each connect round-robins across the N 
worker threads, so roughly 1 in N attempts binds to the dead loop and fails.
   2. **Reused cached client (worse — silent hang):** a pooled 
`TransportClient` pinned to the dead loop still has an open socket, so 
`isActive()` was true and `createClient` kept returning it. 
`writeAndFlush().addListener()` then submits to the dead loop; netty's 
`safeExecute` **swallows** the `RejectedExecutionException` (only logs "Failed 
to submit a listener notification task. Event loop shut down?"), the 
callback/listener is **orphaned**, and the fetch (broadcast/RDD/RPC) **hangs 
forever**.
   
   This PR makes the client network stack self-heal in-process, all within 
`common/network-common`:
   
   - **`TransportClient.isActive()`** returns `false` when 
`channel.eventLoop().isShuttingDown()` is true, so a poisoned pooled client is 
no longer treated as active and is not reused — `createClient` creates a new 
one instead.
   - **`TransportClientFactory.createClient`**, when a connect fails and the 
cause chain contains a `RejectedExecutionException` whose message is exactly 
`"event executor terminated"` (the terminated-loop rejection only — the 
queue-full default handler throws with no message), replaces `workerGroup` with 
a fresh group and rethrows, so the existing `IOException` retry path (e.g. 
`RetryingBlockTransferor`) reconnects onto a fresh, all-live group.
     - `recreateWorkerGroup` is `synchronized` and **identity-guarded** 
(`workerGroup != connectGroup` → no-op), so N concurrent callers that all hit 
the same dead group swap it exactly once. `workerGroup` is `volatile`.
     - The superseded group is **not shut down eagerly** — its still-live 
threads may be serving already-open channels. It is retained via a 
`WeakReference` and shut down best-effort in `close()`; its threads are daemon, 
so a not-yet-collected group cannot block JVM shutdown.
   - Gated by a new config `spark.network.recreateWorkerGroupOnDeadEventLoop`, 
default `true`.
   
   ### Why are the changes needed?
   
   Without this, a single dead netty worker thread degrades the client network 
stack for the lifetime of the JVM: new connections fail ~1/N of the time, and — 
worse — a reused pooled client submits to the dead loop where the rejection is 
swallowed, orphaning the callback so the fetch hangs forever. Only a fresh JVM 
fully clears the poison. Recreating the worker group on the terminated-loop 
rejection lets the existing retry path recover in-process instead.
   
   ### Does this PR introduce _any_ user-facing change?
   
   No. This is an internal reliability fix. It adds an internal-style network 
config `spark.network.recreateWorkerGroupOnDeadEventLoop` (default `true`); 
when disabled, the previous behavior is preserved. When no event loop dies, 
behavior is unchanged.
   
   ### How was this patch tested?
   
   New unit tests in `common/network-common`:
   
   - `TransportClientSuite.isActiveFalseWhenEventLoopIsShuttingDown` — a client 
whose event loop reports `isShuttingDown()` is not active even though the 
channel still reports open/active.
   - `TransportClientFactorySuite.recreatesWorkerGroupWhenEventLoopIsDead` — 
shutting down the factory's worker group makes the next `createClient` fail 
with the terminated-loop rejection; the factory swaps in a fresh live group and 
a subsequent connection succeeds.
   - `TransportClientFactorySuite.doesNotRecreateWorkerGroupWhenDisabled` — 
negative control with the config off: the connect still fails and the worker 
group is left unchanged.
   
   `network-common/testOnly TransportClientSuite TransportClientFactorySuite` 
passes (12 tests). `core` compiles; `network-common` checkstyle (main + test) 
and `core` scalastyle report no issues.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code (Anthropic)
   


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


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to