[ 
https://issues.apache.org/jira/browse/CAMEL-25582?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Claus Ibsen resolved CAMEL-25582.
---------------------------------
    Resolution: Fixed

Fixed by https://github.com/apache/camel/pull/27762 (merged to main as 
05b3e67bb9f2, 4.24.0).

_Claude Code on behalf of Claus Ibsen_

> camel-vertx-websocket - a consumeAsClient consumer starts overlapping 
> reconnect attempts: after a slow handshake it holds up to 20 WebSockets and 
> routes every message once per WebSocket; it also keeps reconnecting after its 
> route is stopped
> ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-25582
>                 URL: https://issues.apache.org/jira/browse/CAMEL-25582
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-vertx-websocket
>            Reporter: shashank
>            Assignee: shashank
>            Priority: Major
>             Fix For: 4.24.0
>
>
> When the WebSocket of a consumer with {{consumeAsClient=true}} is closed, its 
> close handler starts a periodic timer, and every run of the timer starts a 
> connect attempt on a worker thread 
> ({{VertxWebsocketClientConsumer.configureWebSocketHandlers}}, lines 52-85 at 
> main c3829069cb72):
> {code:java}
> vertx.setPeriodic(configuration.getReconnectInitialDelay(), 
> configuration.getReconnectInterval(), timerId -> {
>     vertx.executeBlocking(() -> {
>         getEndpoint().getWebSocket(this::configureWebSocketHandlers);
>         vertx.cancelTimer(timerId);
>         return null;
>     }, false)
> {code}
> {{VertxWebsocketEndpoint.getWebSocket}} (lines 164-203) connects and waits 
> for the handshake for up to the connect timeout of the client options (60 
> seconds by default, line 200). The timer runs again after 
> {{reconnectInterval}} (1 second by default), and nothing checks whether the 
> previous attempt has ended: {{executeBlocking(..., false)}} runs the attempts 
> in parallel, up to the size of the Vert.x worker pool (20 by default).
> *1. Duplicate WebSockets and duplicate delivery.* When the server answers the 
> handshake later than {{reconnectInterval}} (a TCP connect with a 
> retransmitted SYN, a proxy or ingress that waits for its backend, a server 
> that is still starting), all the attempts in flight succeed. Each of them 
> registers the handlers of the consumer on its own WebSocket (the setup 
> callback, line 188). The endpoint keeps the last one in its field; the others 
> stay open until the endpoint stops. From then on every message the server 
> sends to its clients is routed once per WebSocket. When these WebSockets are 
> closed later, each close handler starts a timer of its own, so the next 
> reconnect starts with that many timers.
> *2. Reconnecting does not stop with the consumer.* The consumer has no 
> {{doStop}}. The timer is only cancelled by a successful attempt (line 62) or 
> by {{maxReconnectAttempts}} (0 = unlimited by default). A route that is 
> stopped while the server is away goes on connecting every interval, and the 
> WebSockets of the attempts that succeed after the stop stay open (messages on 
> them were not tested).
> *3.* A connect that completes after the caller stopped waiting (the 
> {{future.get}} timed out, line 200) still registers the handlers (line 188) 
> and its WebSocket is not kept or closed by anyone.
> h3. Reproduction
> New {{VertxWebsocketConsumerAsClientSlowReconnectTest}} (4 tests). The server 
> is a plain Vert.x HTTP server in the test that counts handshakes and 
> WebSockets and can accept a handshake at once, accept it after 1 second, hold 
> it until the test answers it, or reject it. The route is 
> {{from("vertx-websocket:localhost:<port>/feed?consumeAsClient=true&reconnectInterval=50")}}.
>  On main all 4 fail:
> * {{reconnectSlowerThanTheIntervalOpensOneWebSocket}}: the server closes the 
> WebSocket and answers the next handshakes after 1 second; then it sends one 
> message on every WebSocket it has. {{handshakes sent for one reconnect ==> 
> expected: <1> but was: <20>}}, {{WebSockets the consumer has open after one 
> reconnect ==> expected: <1> but was: <20>}}, {{mock://result Received message 
> count. Expected: <1> but was: <20>}}.
> * {{reconnectThatCompletesAfterStopIsClosed}}: the route is stopped while a 
> reconnect is in flight. {{WebSockets pending or open after the consumer was 
> stopped ==> expected: <0> but was: <20>}}.
> * {{stoppedConsumerDoesNotReconnect}}: the server rejects the handshake, the 
> route is stopped. {{Condition with alias 'no reconnect attempt after the 
> consumer was stopped' didn't complete within 10 seconds}} (the handshakes go 
> on).
> * {{restartWhileReconnectIsPendingOpensOneWebSocket}}: the route is stopped 
> and started again while the server holds the handshake of a reconnect, then 
> the server answers. {{handshakes sent by the reconnect and the restart ==> 
> expected: <2> but was: <6>}}, {{WebSockets the consumer has open after the 
> restart ==> expected: <1> but was: <6>}} (6 in this run; the number depends 
> on how many timer runs fit in before the stop).
> h3. Proposed fix
> In {{VertxWebsocketClientConsumer}}:
> * one reconnect timer per consumer: a close event while the timer runs does 
> not start another one (a WebSocket that is found closed when its attempt 
> returns starts the timer again);
> * a run of the timer is skipped while an attempt is in flight;
> * {{doStop}} cancels the timer, and a run of the timer of a stopped consumer 
> cancels it too;
> * start and reconnect connect under one lock of the endpoint (a route that is 
> stopped and started again gets a new consumer, while an attempt of the 
> previous one can still wait for its handshake);
> * an attempt that returns after its consumer was stopped closes the WebSocket 
> it opened, before it releases that lock, so that a start which waits for it 
> connects again.
> In {{VertxWebsocketEndpoint.getWebSocket}}: a connect that completes after 
> the caller stopped waiting closes its WebSocket instead of registering the 
> handlers; the same when the setup callback throws.
> Behaviour change, with an entry in the 4.24 upgrade guide: one attempt at a 
> time, so with a server that does not answer, attempts follow each other (each 
> up to the connect timeout) instead of running in parallel, and 
> {{maxReconnectAttempts}} counts them one after the other; a stopped consumer 
> no longer reconnects.
> Not changed (follow-ups): stopping the route does not close a WebSocket that 
> is open (as on main, it is closed when the endpoint stops; it belongs to the 
> endpoint and a producer on the same endpoint uses it too); the producer side 
> of {{getWebSocket}} is not serialised, so concurrent exchanges that find the 
> WebSocket closed can each open one.
> camel-vertx-websocket with the fix: 107 tests, 0 failures.
> How it was found: a small TLA+ model of the reconnect on main (close events, 
> timer runs, attempts in flight, stop), with the assumption that an attempt 
> can still be in flight when the timer runs again. The model checker finds 
> traces of the model in which the consumer is registered on two open 
> WebSockets (close, two timer runs, both attempts succeed) and in which the 
> timer runs after the stop. This reproduces the defect on a model of main; it 
> is not a proof of the fix. The defect was then confirmed with the real 
> consumer as described above.
> Affected: main, camel-4.23.x, camel-4.22.x, camel-4.18.x and camel-4.14.x. 
> Verified by reading {{VertxWebsocketClientConsumer}} on each branch: the same 
> {{setPeriodic}} / {{executeBlocking}} / connect / {{cancelTimer}} at lines 
> 59-62, and no {{doStop}}. Not run on the maintenance branches.
> Duplicate check (2026-10-12): JIRA text "vertx-websocket reconnect" (1: 
> CAMEL-23889, flaky tests), "VertxWebsocketClientConsumer" (1: CAMEL-20011, 
> deprecated executeBlocking), "consumeAsClient" (1: CAMEL-20656, query 
> parameters), "vertx-websocket reconnectInterval" (none), "vertx-websocket 
> consumeAsClient duplicate" (none); GitHub pull requests "vertx-websocket 
> reconnect": #24393 (merged, CAMEL-23889), #24740 (merged, CAMEL-24126), none 
> open.
> _Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to