shashank created CAMEL-26571:
--------------------------------
Summary: camel-consul - ConsulClusterView can be leader after it
was stopped, and a view stopped while Consul cannot be reached never takes part
in the leader election again
Key: CAMEL-26571
URL: https://issues.apache.org/jira/browse/CAMEL-26571
Project: Camel
Issue Type: Bug
Components: camel-consul
Reporter: shashank
Assignee: shashank
Found with a TLA+ model of {{ConsulClusterView}} (two nodes, each with its
lifecycle thread and the Consul client thread that handles the answers of the
blocking query; Consul answers, session expiry and failing calls chosen
nondeterministically), then reproduced on the real classes with mocked Consul
clients (no Consul).
A stopped view must not be leader, and a view that is started again must take
part in the leader election with a new session. Three ways break this:
- *A stop or a failed start while Consul cannot be reached.* {{doStop}} calls
{{releaseLock}} and then {{destroySession}}; if one of them throws,
{{setMaster(false)}} is skipped and the stop fails. If {{releaseLock}} throws,
the session id is also kept, so a later start does nothing ({{doStart}} only
runs when there is no session id): no new session, no watch, and the local
member keeps the leadership it had. Its old session is not renewed anymore,
Consul invalidates it after its TTL and another node takes the lock, so two
nodes are leader for good. A start that fails after it created the session is
stopped by {{BaseService}}, with the same result. For example a {{master:}}
route stopped and started again while the Consul agent restarts: the restarted
view still says leader, {{MasterConsumer}} starts its consumer, and the
consumer of the new leader runs as well. A view that was not leader never
becomes leader again.
- *An answer handled while the view stops.* {{Watcher.onComplete}} checks
{{isStarting() || isStarted()}}; when the key is free it then acquires the lock
(under {{sessionIdLock}}) and calls {{setMaster}} after releasing
{{sessionIdLock}}. A {{doStop}} that waited for {{sessionIdLock}} destroys the
session and calls {{setMaster(false)}} in between, and the callback then sets
the leadership: the stopped view is leader and fires a leadership event. After
a forced stop of the view (JMX {{stopView}}) the clustered routes of a
{{ClusteredRoutePolicy}} start on this node, while another node takes the lock
that was released.
- *An answer to a query of an earlier start.* The check only looks at the
status, so an answer that is handled after the view was stopped and started
again is applied to the new session (a stale "held by the old session" turns
the leader of the new session into a follower until the next answer; a late
failure of the old query does the same), and the callback queries again: one
more watch, and one more session renewal every interval, for each quick stop
and start.
Short traces of the model:
{noformat}
stop while Consul cannot be reached answer handled while the view stops
1 n1 start: leader with session s1 1 n2 held the lock and released
it; n1 Consul thread:
2 n1 doStop: releaseLock throws (L131) answer "key free", running
(L305, L309)
-> stop fails, sessionId = s1, leader 2 n1 doStop: releaseLock (nothing
held, L131), waits
3 n1 start: sessionId != null -> no-op (L98) for sessionIdLock (L134)
4 s1 not renewed, expires; n2 takes the lock 3 n1 Consul thread: acquireLock ->
true (L312)
-> n1 and n2 leader 4 n1 doStop: destroySession(s1),
setMaster(false)
(L136-137)
5 n1 Consul thread:
setMaster(true) (L315)
-> stopped view is leader
{noformat}
The real-class tests (mocked {{KeyValueClient}} and {{SessionClient}}) stop the
view while the clients throw, fail a start right after the session was created,
let only the lock release fail, apply the answer and the failure of the query
of the first start after a restart, and stop the view from a hook right after
the watch answer acquired the lock. A probe on the old code shows the stop case
end to end: the restarted view keeps the old session, sends no query and stays
leader, and another node then takes the free lock.
*Proposal:* {{doStop}} releases the lock and destroys the session, each best
effort (a failure is logged; Consul invalidates the session after its TTL) and
always clears the session id and the leadership. A watch answer decides and
sets the leadership under {{sessionIdLock}}, which {{doStop}} now holds for the
release, the destroy and {{setMaster(false)}}, and only if its watch is the one
of the running view: every start creates a new watch, and an answer of an older
one is ignored and does not query again.
Affected: main and 4.23.x (the same code); 4.22.x, 4.18.x and 4.14.x have the
same {{doStop}} and the same check in {{onComplete}}. The fix builds on
CAMEL-25286 (main and 4.23.x only), so the older branches need their own patch.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)