shashank created CAMEL-26574:
--------------------------------
Summary: camel-netty - Client mode consumer: stop leaves the
connection and the reconnect running with a shared worker group, and the
reconnect blocks the event loop
Key: CAMEL-26574
URL: https://issues.apache.org/jira/browse/CAMEL-26574
Project: Camel
Issue Type: Bug
Components: camel-netty
Reporter: shashank
Assignee: shashank
Found with a TLA+ model of {{ClientModeTCPNettyServerBootstrapFactory}} (the
lifecycle thread starting and stopping the consumer, the event loops running
the close listener and the reconnect task, connect outcomes and the server
closing the connection chosen nondeterministically; a second model with two
consumers on one shared group of two event loops), then reproduced on the real
classes against an embedded Netty server on localhost.
A consumer with {{clientMode=true}} connects to a server and, with
{{reconnect=true}} (the default), connects again when the connection is lost.
Several things go wrong:
*1. A stop does not close the connection when the worker group is shared.*
{{stopServerBootstrap()}} never closes the channel; it only shuts down a worker
group the factory created itself. With {{workerGroup=#myGroup}} (the documented
way to share event loops between routes) the stopped consumer keeps its
connection: whatever the server sends after the stop is read and dropped (the
server sees it as delivered). The close listener and the scheduled reconnect
stay active too:
* a reconnect scheduled before the stop (the server was down at start, or had
just closed the connection) connects after the stop;
* when the server closes the leftover connection after the stop, the stopped
consumer connects again;
* a route restart opens a second connection while the first one stays open, and
both feed the route.
TLA+ trace (main): start while the server is down (reconnect scheduled), stop:
after the stop the reconnect is still scheduled ({{StopNoReconnect}} violated
in 5 steps); start, stop, start: two open connections ({{OneConnection}}
violated).
*2. The reconnect waits on an event loop.* The reconnect task runs on an event
loop and calls {{openChannel}}, which waits for the connect with
{{awaitUninterruptibly()}}.
* When the new channel gets the same event loop ({{workerCount=1}}, or a shared
group whose round robin returns the loop of the task), Netty throws
{{BlockingOperationException}}. The task logs "Will attempt again in ...
millis. This exception is ignored." but schedules nothing; the connect goes on
without the close listener, so after the next connection loss the consumer
never reconnects.
* When two reconnect tasks on a shared group of two loops (two consumers whose
server restarted, or a stopped consumer's leftover reconnect and the restarted
one) each connect a channel on the loop of the other, both loops wait for a
connect that only the other loop can complete. The connect timeout is scheduled
on the same blocked loops, so this never ends: every channel of the shared
group (other routes, producers) hangs, and a route start waiting for its
connect on one of these loops never returns (nor does the shutdown of the
context, which waits for that start).
TLA+ trace (two consumers, shared group of two loops): both connections drop;
the reconnect of consumer 2 (loop 2) connects a channel on loop 1 and waits;
the reconnect of consumer 1 (loop 1) connects a channel on loop 2 and waits:
{{NoLoopDeadlock}} violated in 5 steps. Another trace: a reconnect whose
channel lands on its own loop ends the reconnects of that consumer
({{ChainsKept}} violated in 3 steps).
Reproduced on main (1b12eea752e6) with an embedded Netty server on localhost:
* {{workerGroup=#shared}}, route stopped: the server side of the connection is
still open 10 s later; with {{reconnect=true}}, the server closing that
connection after the stop is followed by a new connection from the stopped
consumer; after a stop and a start two connections are open and messages on
both reach the route.
* {{workerCount=1}}: the consumer connects once and never again after the
server closes the connection ({{BlockingOperationException}} in the log).
* Two client consumers on one shared group of two loops, the server closing
both connections at once five times: fails 20 of 20 runs.
* A hook-free loop of 200 stop/start cycles (shared group, the server closing
every connection after 0-3 ms): a connection after the stop in 105 of the first
110 cycles, then the start of the next cycle hung on the event loop deadlock
(the thread dump shows both loops in {{openChannel}} ->
{{awaitUninterruptibly}}). With the change: 0 of 200, no hang.
*Proposal*
* {{stopServerBootstrap}} cancels the scheduled reconnect and closes the
channel and a connect in progress, and waits for the close (up to
{{shutdownTimeout}}, as the server mode does, not when called on the event loop
of the channel).
* The reconnect does not wait on the event loop: the connect outcome is handled
by a listener (track the channel and its close listener, or schedule the next
reconnect).
* The reconnect state (channel, connect in progress, scheduled task, stopping
flag, a generation per start) is guarded by one lock; the close listener, the
reconnect task and the connect listener act only if the consumer is not
stopping and belongs to the current start, so a stop racing a reconnect leaves
nothing behind and a restart cannot run two reconnect chains.
* No option or default changes. The code is the same on camel-4.23.x, 4.18.x
and 4.14.x.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)