shashank created CAMEL-25655:
--------------------------------
Summary: camel-zookeeper - after the ZooKeeper session expired the
consumer waits for ever without any Camel log line, and the producer fails
every message, until the route is restarted
Key: CAMEL-25655
URL: https://issues.apache.org/jira/browse/CAMEL-25655
Project: Camel
Issue Type: Bug
Components: camel-zookeeper
Reporter: shashank
Assignee: shashank
A ZooKeeper session expires when the client cannot reach the ensemble for
longer than the session timeout (the component's {{timeout}} option, 5000 ms by
default): a network partition, a long GC pause, a suspended VM, an ensemble
that loses its quorum for a few seconds. The server then expires the session;
when the client reaches it again it is told so, the {{ZooKeeper}} handle is
closed for good (state {{CLOSED}}), and every watcher of the handle gets an
event of type {{None}} with state {{Expired}}. A new handle is needed.
Lines at main 1de5ec5135f8:
* {{FutureEventDrivenOperation.process}} (operations, 41-60 in the file)
releases the waiting thread only for one of its awaited event types
({{NodeDataChanged}}/{{NodeDeleted}}, {{NodeCreated}}/{{NodeDeleted}},
{{NodeChildrenChanged}}); {{get()}} waits on the latch without a timeout. The
{{Expired}} event is ignored.
* {{ConnectionHolder.getZooKeeper}} returns the cached handle whenever it is
not null, also when it is closed; nothing in the module creates a new one while
the route runs.
Consequences:
* Consumer: its operations thread waits in {{DataChangedOperation}} /
{{ExistenceChangedOperation}} / {{ChildrenChangedOperation}} for an event that
can no longer come. The ZooKeeper client logs one WARN ("Unable to reconnect to
ZooKeeper service, session 0x... has expired"); after that the route stays
started, consumes nothing, and Camel logs nothing (the {{Expired}} event is
only logged at DEBUG). This hits the configuration for watching a node
continuously: {{repeat=true}}.
* The same wait for ever when the expiry is noticed while no watch of the
consumer is registered, for example while the operations thread routes an
exchange: the next watch request on the closed handle is answered with
{{SESSIONEXPIRED}} to the request's callback only
({{ClientCnxn.conLossPacket}}), the watch is not registered and no {{Expired}}
event reaches it - and the callbacks of the three operations are empty. The
longer the route takes, the more likely this is.
* Producer: every message fails with
{{KeeperException.SessionExpiredException}} until the route is restarted.
A route stop / start creates a new consumer and the endpoint's connection
manager a new handle ({{doStop}} closes it), so a route restart does recover;
that is the only way out on main.
h3. Reproduction
New {{ZooKeeperConsumerSessionExpiryTest}} with a ZooKeeper server in the test
JVM ({{ZooKeeperServer}} + {{NIOServerCnxnFactory}} from the zookeeper jar).
The session of the Camel handle is expired the usual way: a second client opens
the same session (id and password) and closes it. On main:
* consumer ({{repeat=true&backoff=200}}): the node is updated after the expiry;
{{the consumer did not consume the update made after its session had expired
==> expected: <true> but was: <false> within 20 seconds}};
* the same, with the expiry while the route's processor holds the operations
thread: {{the consumer did not consume the update made after its session had
expired during an exchange ...}};
* consumer with {{repeat=false}}: {{the consumer did not read the data again
after its session had expired ==> expected: <2> but was: <1> within 20
seconds}} (see the behaviour after the fix below);
* producer: a request after the expiry: {{the producer failed after its session
had expired ==> expected: <null> but was:
<org.apache.zookeeper.KeeperException$SessionExpiredException: KeeperErrorCode
= Session expired for /camel-session-expiry>}}.
With the fix the module log shows the event reaching the operation ("The
ZooKeeper session expired while waiting for an event on node ..."), the failed
watch request ("Could not watch node ...") and the new session ("The ZooKeeper
session 0x... can no longer be used (CLOSED). Creating a new session.").
h3. Proposed fix
* {{FutureEventDrivenOperation}}: on ({{None}}, {{Expired}}) stop waiting and
fail with an {{ExecutionException}} caused by {{SessionExpiredException}}. The
callbacks of the watch requests of the three operations fail the operation the
same way when the result code is not {{OK}} ({{NONODE}} is the normal result of
the exists watch and is not a failure there). The consumer hands the failure to
its exception handler (one WARN per expiry) and starts the sequence of
operations again after the configured {{backoff}}, as it does for any failed
operation.
* {{ConnectionHolder.getZooKeeper}}: when the cached handle is no longer alive
({{CLOSED}} or {{AUTH_FAILED}}), close it and create a new handle; each handle
counts down its own connection latch.
* {{ZooKeeperConsumer}}: take the connection again before the sequence is
started again, and do not queue a copy of the failed operation in addition to
the new sequence (on main every restart left that extra copy in the queue;
after an expiry it would wait on the closed handle).
* {{ZooKeeperProducer}}: take the connection for each message (a cheap check
when the handle is alive).
Behaviour after the fix: the WARN is "The ZooKeeper session expired while
waiting for an event on node ...", "Could not watch node ..." when the expiry
was noticed before the watch request, or "All operations exhausted without one
result" (no cause) for a consumer of a node that does not exist yet
({{AnyOfOperations}} hides the cause). The restarted sequence reads the node
again, so after every expiry the consumer emits the node's current data once
more, also when it did not change and also with {{repeat=false}}; changes made
during the outage are seen only as the latest value.
No option or default changes, no upgrade guide entry. The new test needs
{{metrics-core}} and {{snappy-java}} with test scope (zookeeper declares them
{{provided}}; the embedded server loads them); the {{snappy-java}} version is a
new property in the parent pom.
camel-zookeeper unit tests with the fix: 10 run, 0 failures, 0 skipped. Not
run: the module's integration tests (they need a container), an ensemble of
several servers.
How it was found: a sweep of the reconnect code of the components, then a small
TLA+ model (handle alive / closed, the operations thread waiting / in backoff,
expiries, updates). For main it reaches the thread waiting on a closed handle
after one expiry, and a run that never watches again; with the fix both
properties hold for two expiries (27 states). The model is a sketch; the tests
are the evidence.
Not changed: a stopped and started consumer instance (only reachable through a
route policy or user code, not through a route restart) still adds no
operations, because {{shuttingDown}} is never reset; the thread that creates a
new handle waits for its first event without a timeout, as at start (a producer
message sent while the ensemble is still unreachable after an expiry waits
instead of failing at once).
Affected: main (1de5ec5135f8; tested). camel-4.23.x, camel-4.22.x, camel-4.18.x
and camel-4.14.x have the same {{FutureEventDrivenOperation.process}},
{{ConnectionHolder.getZooKeeper}} and empty watch callbacks (read, not run).
Duplicate check (2026-10-14): JIRA text "zookeeper expired" (CAMEL-8208, route
policy, 2015), component camel-zookeeper recent issues (cluster view and master
only); GitHub pull requests "ZooKeeperConsumer", "zookeeper session expired",
"camel-zookeeper consumer" (#25164 moved the backoff to BackgroundTask; nothing
about expiry).
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)