shashank created CAMEL-25613:
--------------------------------
Summary: camel-jmx - consumer with
reconnectOnConnectionFailure=true closes and replaces a working connection
every reconnectDelay for ever after a lost connection
Key: CAMEL-25613
URL: https://issues.apache.org/jira/browse/CAMEL-25613
Project: Camel
Issue Type: Bug
Components: camel-jmx
Reporter: shashank
Assignee: shashank
All of this is about a remote JMX consumer with
{{reconnectOnConnectionFailure=true}}. The option is off by default; it is the
option one sets so that the consumer survives an outage of the monitored JVM,
and the defect follows every such outage. {{JMXConsumer}}, lines at main
a7169d7c6ab6:
* {{initNetworkConnection}} (139-154) closes the previous connector (142),
connects (150), adds the connection listener (151) and stores the connection id
(152);
* {{ConnectionNotificationListener.handleNotification}} (190-207) ignores a
notification whose connection id is not the stored one (193) and calls
{{scheduleReconnect()}} for {{NOTIFS_LOST}}, {{CLOSED}} and {{FAILED}}
(196-201);
* {{scheduleReconnect}} (213-226) schedules one task per call; the task calls
{{initNetworkConnection()}} and {{addNotificationListener()}}, and any
exception schedules it again (220).
h3. 1. One lost connection schedules two reconnects, and the replacement never
stops
The RMI connector of the JDK reports a connection that can no longer be used
with {{jmx.remote.connection.failed}} and then, from the close that follows,
with {{jmx.remote.connection.closed}}, both with the id of that connection
(observed with JDK 21;
{{RMIConnector.RMIClientCommunicatorAdmin.gotIOException}} sends the first and
calls {{close(true)}}, which sends the second). Both pass the id check (193),
so two reconnect tasks are scheduled.
When the server is back, the first task connects and subscribes. The second
task then closes that working connector (142). Closing a connector whose
connection works sends {{jmx.remote.connection.closed}} with the id of that
connection to the connection listener, synchronously, inside {{close()}}; the
id is still the stored one (it is replaced at 152), so the listener schedules a
third task; and so on. Result: after an outage the consumer closes and replaces
a working connection every {{reconnectDelay}} (10 seconds by default) for as
long as the route runs, with a WARN "Lost JMX connection" each time, and
notifications that the MBean sends between a close and the next subscription
are lost.
Observed in a local run with main's {{JMXConsumer}}, a JMX server in a second
JVM that was killed and started again on the same port, and
{{reconnectDelay=1}}: the connector delivered {{failed}} and {{closed}} with
the stored id from one thread, the consumer logged "Lost JMX connection" and
"Delaying JMX consumer reconnection" twice, and the new server accepted 16
connections from the consumer in the first 8 seconds (two every second).
The "closed" notification of its own connector keeps the replacement going in
two more cases:
* after one {{NOTIFS_LOST}} (the server dropped notifications for this client;
the connection itself is fine);
* when an attempt connected but {{addNotificationListener}} failed, typically
with {{InstanceNotFoundException}}: the JMX connector of a restarting
application is usually reachable before the application has registered its
MBeans.
h3. 2. The consumer does not follow the connection id after the connector
reconnected by itself
When the server object is gone but the server still answers
({{NoSuchObjectException}}, for example a connector server that was stopped and
started again inside a JVM that keeps running), the RMI connector tries once to
re-establish its connection. If that works it sends
{{jmx.remote.connection.opened}} with a new connection id, and it adds again
only the listeners whose MBean exists at that moment (read in the JDK sources:
{{RMIClientCommunicatorAdmin.doStart}} and {{reconnectNotificationListeners}}).
The consumer ignores that notification and keeps the old id, so:
* every later {{FAILED}} / {{CLOSED}} / {{NOTIFS_LOST}} carries the new id and
is ignored (193): the consumer never reconnects again, although
{{reconnectOnConnectionFailure=true}}, and nothing is logged;
* when the MBean was not registered at the moment of the reconnection, the
consumer has a connection and no subscription, receives nothing, and logs
nothing.
h3. 3. A notification after stop reconnects the stopped consumer
The lost-connection branch of the listener (196-201) does not check whether the
consumer still runs. {{doStop}} shuts the scheduled executor down and forgets
it (266-269); a notification that is handled after that makes {{getExecutor()}}
(232-238) create a new executor, and the stopped consumer connects again.
h3. Reproduction
New {{JMXConsumerReconnectTest}} (RMI registry and connector server in the test
JVM, set up like the existing {{JMXRobustRemoteConnectionTest}};
{{reconnectDelay=1}}; the connector server counts the client connections it
accepts). On main all five tests fail:
* {{FAILED}} and then {{CLOSED}} with the current connection id are given to
the connection listener: one new connection is expected and then none for three
seconds. On main: {{the consumer connected again although it was subscribed on
a working connection ==> expected: <false> but was: <true>}};
* a {{NOTIFS_LOST}} notification for the current connection: one new connection
and then none. On main the same message;
* the server is stopped and started again without the MBean, the test waits for
two new connections, registers the MBean and waits for a notification; after
that the connections must stop. On main: {{the consumer kept connecting
although it was subscribed on a working connection}} (when the connector
reported the loss) or {{the consumer did not keep trying to subscribe after the
server came back without the MBean}} (when the connector reconnected by itself;
which of the two happens depends on timing);
* an {{OPENED}} notification with another connection id: the consumer must open
a connection of its own again. On main: {{the consumer did not subscribe again
after the connector had reconnected by itself ==> expected: <true> but was:
<false>}};
* the route is stopped and {{FAILED}} with the last connection id is given to
the listener: no connection may follow. On main: {{the consumer connected again
although it was stopped ==> expected: <false> but was: <true>}}.
h3. Proposed fix
* {{scheduleReconnect}} schedules at most one reconnect at a time: a flag is
set with {{compareAndSet}} when a reconnect is scheduled, cleared when the task
begins (so that a failure reported during the attempt schedules the next one)
and reset in {{doStop}}. It schedules nothing when the consumer is not allowed
to run, and the task ends at once in that case.
* {{initNetworkConnection}} removes the connection listener from the previous
connector before it closes it.
* An {{OPENED}} notification with another id: the consumer stores the new id
and, with {{reconnectOnConnectionFailure=true}}, schedules its normal
reconnect, which subscribes again on a new connection (it cannot know whether
the connector kept its listener). A WARN line is logged; with
{{reconnectOnConnectionFailure=false}} only a WARN line that the consumer may
not be subscribed anymore.
* The listener ignores a lost connection when the consumer is not allowed to
run.
* The fields that the notification threads read are volatile.
One consequence to know: when the connector re-established the connection by
itself and all listeners were added again, main keeps receiving without a gap
(and is deaf to later failures). With the fix the consumer replaces that
connection once, {{reconnectDelay}} later; notifications that the MBean sends
between the close and the new subscription are lost that one time.
Not changed: with {{testConnectionOnStartup=false}} a delayed start that
connects but cannot add the listener still logs "An unrecoverable exception has
occurred" at ERROR and does not try again; {{doStop}} still fails when the
server cannot be reached; a reconnect attempt that is already connecting when
the consumer is stopped is not interrupted and may leave a connection open; the
WARN "Lost JMX connection" is still logged for each of the two notifications.
camel-jmx tests with the fix: 58 run, 0 failures, 0 skipped. With the fix and
the same second-JVM run as above the new server accepted one connection and the
next notification of the MBean was delivered. Not run: a JMX server on another
host.
How it was found: a sweep of the reconnect code of the components, then a small
TLA+ model (server up or down, MBean registered, connector state, stored id,
pending and running reconnect, the "failed" and "closed" pair, stop). For main
TLC reports two pending reconnects after one lost connection (4 steps), a
replaced connection without a cause, "nothing pending, server up, MBean
registered, not subscribed", a run that never ends subscribed with nothing
pending, and a reconnect scheduled after stop. With the fix the properties hold
for three environment events and one stop (739 states). The model is a sketch;
the tests are the evidence.
Affected: main (a7169d7c6ab6; tested). camel-4.23.x, camel-4.22.x, camel-4.18.x
and camel-4.14.x have the same id check, the same {{initNetworkConnection}} and
the same {{scheduleReconnect}} (read, not run).
Duplicate check (2026-10-13): JIRA text "jmx reconnectOnConnectionFailure"
(CAMEL-5613), "JMXConsumer reconnect" (CAMEL-19527, tests only), "jmx consumer
connection notification" (nothing related); GitHub pull requests "JMXConsumer",
"camel-jmx reconnect": #2052 (2017, close the connector on shutdown), none for
this.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)