shashank created CAMEL-25286:
--------------------------------
Summary: camel-consul - ConsulClusterView stops taking part in the
leader election after a failed query or when Consul invalidates its session,
and can keep the leadership while another node takes it
Key: CAMEL-25286
URL: https://issues.apache.org/jira/browse/CAMEL-25286
Project: Camel
Issue Type: Bug
Components: camel-consul
Reporter: shashank
{{ConsulClusterView}} (used by {{ConsulClusterService}}, {{camel-master}} and
{{ClusteredRoutePolicy}}) holds the leadership with a lock on
{{<rootPath>/<namespace>}} owned by a Consul session. A {{Watcher}} watches
that key with a chain of blocking queries; each query also renews the session.
Three paths end the election for the node until the cluster service is
restarted:
h3. 1. A failed query: the watch ends and the node can stay leader while
another node takes the lock
{code:java}
public void onFailure(Throwable throwable) {
LOGGER.debug("{}", throwable.getMessage(), throwable);
if (sessionId.get() != null) {
keyValueClient.releaseLock(configuration.getRootPath(),
sessionId.get()); // wrong key, synchronous
}
localMember.setMaster(false);
watch();
}
{code}
* It releases the lock of the root path ({{/camel}}), not of the namespace path
that the session holds (the same mistake was fixed in {{getLeader()}} in 2021,
PR #4937).
* {{releaseLock}} is a synchronous HTTP call. When the query failed because
Consul cannot be reached (the agent restarts, a network error), that call
throws a {{ConsulException}}, so {{setMaster(false)}} and {{watch()}} never
run: no more queries and no more session renewals. A leader keeps its
leadership locally (its clustered routes keep running); its session expires
after the TTL, Consul releases the lock, another node acquires it, and both
nodes run the clustered routes. A node that was not leader never takes part
again.
* The same happens when handling an answer fails ({{acquireLock}} and the
leadership event call Consul synchronously from {{onComplete}}), and
{{setMaster(false)}} itself calls Consul ({{getLeader()}}) to fire the
leadership event, so the event was lost while Consul was down.
h3. 2. An invalidated session is never replaced
When Consul invalidates the session (its TTL expired during a network
partition, the local agent restarted or left so its {{serfHealth}} check
failed, Consul lost its data), every renewal fails with {{404 Session id '...'
not found}} (the stack trace of CAMEL-15423), and {{acquireLock}} returns
{{false}} for ever because {{getSessionInfo}} finds no session. The node can
never be leader again; if all nodes lost their sessions (for example a rolling
restart of the Consul agents) no node is leader any more and the clustered
routes stay stopped everywhere.
h3. 3. A key that does not exist is ignored
{{onComplete}} only acts when the key exists ({{if (value.isPresent())}}).
After Consul lost its data the key is gone and no node acquires the lock again;
a node that was leader also keeps its leadership locally.
h3. Reproduction (unit test, no Consul)
{{ConsulClusterViewRecoveryTest}} runs the real {{ConsulClusterService}} and
view against mocked {{SessionClient}} / {{KeyValueClient}} that simulate Consul
(sessions, lock holder, key, reachability; a renewal of an unknown session
throws {{ConsulException}} with code 404 as the client does). On main all 4
tests fail:
* Consul cannot be reached, the query fails: the node stays leader ({{expected:
<false> but was: <true>}}) and never queries again.
* A failed query: the lock released is not the one of the namespace path
(Mockito: {{Argument(s) are different! Wanted: releaseLock("/camel/my-ns",
"session-1")}}, actual {{"/camel"}}).
* Consul invalidated the session: no new session is created ({{expected: <2>
but was: <1>}}) and the node never takes the free lock again.
* Consul lost its data: the node stays leader without a lock ({{expected:
<false> but was: <true>}}) and never creates the key again.
h3. Proposed fix
* {{onFailure}}: give the leadership up first, release the lock of the
namespace path (ignoring a failure), and query again after
{{sessionRefreshInterval}} (at least one second) on a single-thread scheduler
of the view, so that an agent that cannot be reached is not queried in a loop.
* Renewal: a session that Consul does not know any more (404, or no session
returned) is replaced by a new one (under the session lock, only while the view
runs), after giving the leadership up; other renewal failures are logged at
debug and retried with the next query.
* {{onComplete}}: a missing key is handled like a free key (try to acquire it);
an exception while handling the answer gives the leadership up instead of
ending the watch. The index is stored before handling the answer.
* The leadership event is fired with no leader when the current leader cannot
be read.
A failed query while Consul can be reached now really releases the lock (no
{{lock-delay}} on an explicit release), so the leadership can move to another
node; the node had already given it up locally on main. While the Consul
cluster has no leader the release fails too and the node takes its own lock
back with the next answer. File lock and ZooKeeper cluster views also give the
leadership up on the first failure.
No new option. With the fix the 4 new tests pass and the module's unit tests
pass (8); the cluster ITs need Docker (not run).
Affected: 4.14.x, 4.18.x and main (same code).
Duplicate check (2026-10-03): JIRA text "ConsulClusterView" /
"ConsulClusterService" / "consul" + "leader" / "consul" + "session" (14
issues): only CAMEL-15423 (2020, closed Incomplete, same symptom, no fix),
CAMEL-22523 (test automation), CAMEL-25275 (consumers, open, ours). GitHub pull
requests "ConsulClusterView": only #25842 (ZooKeeper cluster shutdown) and
#4937 (getLeader path, 2021).
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)