He-Pin opened a new issue, #3135:
URL: https://github.com/apache/pekko/issues/3135

   ## Summary
   
   `TcpOutgoingConnection` retries a failed `finishConnect` by scheduling a 
callback
   on the system scheduler that calls `channelRegistry.register(channel, 
OP_CONNECT)`
   directly, outside the actor's message loop. If that callback throws, the 
exception
   is caught and reported by the scheduler / dispatcher and **never re-enters 
the
   actor's supervision**, so `Tcp.CommandFailed` is never delivered and the
   commander can stay stuck in the `connecting` state indefinitely.
   
   ## Code location
   
   `actor/src/main/scala/org/apache/pekko/io/TcpOutgoingConnection.scala`
   
   ```scala
   // connecting(registration, remainingFinishConnectRetries)
   case ChannelConnectable =>
     reportConnectFailure {
       if (channel.finishConnect()) { ... }
       else {
         if (remainingFinishConnectRetries > 0) {
           context.system.scheduler.scheduleOnce(1.millisecond) {
             channelRegistry.register(channel, SelectionKey.OP_CONNECT)   // 
<-- off-loop
           }(context.dispatcher)
           context.become(connecting(registration, 
remainingFinishConnectRetries - 1))
         } else {
           stop(FinishConnectNeverReturnedTrueException)
         }
       }
     }
   ```
   
   The surrounding `reportConnectFailure { ... }` only covers the body of the
   `ChannelConnectable` handler — it does **not** cover the scheduled lambda.
   
   ## Why this is a problem
   
   `channelRegistry.register` (the `SelectorBasedChannelRegistry` in
   `SelectionHandler.scala`) dispatches a `Task` to the selector's execution
   context whose `tryRun` only catches `ClosedChannelException`. Any other
   throwable (e.g. `IllegalArgumentException` from an invalid ops mask,
   `CancelledKeyException` if the key races with `postStop` shutdown,
   `IllegalBlockingModeException`, or a JVM/NIO bug-induced `RuntimeException`)
   propagates out of the task, is handed to `executionContext.reportFailure`, 
and
   is logged — but:
   
   - it does **not** enter `reportConnectFailure`,
   - it does **not** trigger `stop(e)`,
   - it does **not** emit `Tcp.CommandFailed(connect).withCause(e)` to the
     commander,
   - the `TcpOutgoingConnection` actor keeps running in the `connecting` state
     with the retry counter already decremented, and the commander can wait
     forever for `Connected` / `CommandFailed`.
   
   In practice this has been observed in the wild on akka.net under an analogous
   code shape — see
   
[akkadotnet/akka.net#8195](https://github.com/akkadotnet/akka.net/issues/8195)
   for the original report and the failure sequence. The Pekko code path has the
   same structural issue: a raw scheduled callback performs I/O work that is not
   wrapped in the actor's failure-reporting envelope.
   
   ## Expected behaviour
   
   Any exception thrown by the scheduled retry should surface to the commander 
as
   `Tcp.CommandFailed` and the connection actor should stop cleanly, exactly as
   happens for exceptions thrown inside the normal `receive` path.
   
   ## Suggested fix
   
   Schedule the retry as a self-message so it runs inside the actor's message
   loop and is covered by the existing `reportConnectFailure` envelope — e.g.
   introduce an internal `RetryRegister` message and, in `connecting`, handle
   `case RetryRegister => reportConnectFailure { 
channelRegistry.register(channel, SelectionKey.OP_CONNECT) }`.
   Using `Timers.StartSingleTimer` (as `TcpListener` does in the same module) is
   also preferable to `context.system.scheduler.scheduleOnce`, since the pending
   timer is cancelled automatically when the actor stops, removing the latent
   "register against a channel that is already being torn down" window.
   
   ## Environment
   
   - Pekko version: current `main`
   - Module: `actor` (`org.apache.pekko.io.Tcp*`)
   - JDK/OS: any — the bug is structural; triggering it depends on when
     `channel.register` throws a non-`ClosedChannelException` throwable.
   
   ## Reproduction notes
   
   The akka.net report reproduces the swallowed-exception path by pre-seeding 
DNS
   so the host resolves to both IPv4 and IPv6, forcing an IPv4-only socket, and
   letting the IPv6 fallback retry throw a platform-wide 
`NotSupportedException`.
   A symmetric Pekko test would force the scheduled `channelRegistry.register`
   call to throw (e.g. by injecting a `ChannelRegistry` whose `register` throws
   `IllegalArgumentException`) and assert that the commander still receives
   `Tcp.CommandFailed` rather than hanging.
   
   ## References
   
   - Original field report and analysis: 
https://github.com/akkadotnet/akka.net/issues/8195
   - Pekko source: 
`actor/src/main/scala/org/apache/pekko/io/TcpOutgoingConnection.scala`,
     `actor/src/main/scala/org/apache/pekko/io/SelectionHandler.scala`


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