[ 
https://issues.apache.org/jira/browse/CAMEL-25062?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18119951#comment-18119951
 ] 

shashank commented on CAMEL-25062:
----------------------------------

PR: https://github.com/apache/camel/pull/26945

The PR changes when the routes of a {{ClusteredRoutePolicy}} are started and 
stopped (shortly after the leadership event, on a policy thread that exits when 
idle, instead of inside the event), so feedback on the approach is welcome 
before it is merged.

I cannot assign issues to myself; could a committer assign this to me 
(smjainblr)? Thanks.

_Claude Code on behalf of allthingssecurity_

> camel-cluster - ClusteredRoutePolicy: CamelContext stop or removeRoute can 
> deadlock with a leadership change that is starting routes (follow-up to 
> CAMEL-24545)
> ---------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-25062
>                 URL: https://issues.apache.org/jira/browse/CAMEL-25062
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-core
>            Reporter: shashank
>            Priority: Minor
>
> CAMEL-24545 reported a JVM that hangs on shutdown: 
> {{ClusteredRoutePolicy.onRemove}} and the ZooKeeper leader selector thread 
> take the same two locks in opposite order. The fix (PR #25842, in 4.18.5, 
> 4.22.1 and 4.23.0) changed only {{ZooKeeperClusterView}}, so that it no 
> longer fires a leadership event while it stops. The reviews on that PR 
> described the root cause as the AB-BA inversion between the 
> {{ClusteredRoutePolicy}} lock and the {{StampedLock}} of 
> {{AbstractCamelClusterView}}, accepted the ZooKeeper guard as the minimal 
> fix, and suggested a follow-up for the other cluster views. The inversion 
> itself is still in {{ClusteredRoutePolicy}} on main, for every cluster 
> service (file, kubernetes, consul, infinispan, jgroups-raft) and for 
> ZooKeeper events other than the stop event.
> h3. The lock orders (main, 65f315628)
> * *Event dispatch.* {{AbstractCamelClusterView.fireLeadershipChangedEvent}} 
> calls every listener while it holds the read lock of the view 
> ({{AbstractCamelClusterView.java:111-122}}). The listener of the policy 
> ({{ClusteredRoutePolicy.java:376}}) calls {{setLeader}}, which takes the 
> policy lock ({{:267}}) and starts or stops the routes on the same thread. 
> {{startRoute}} ({{:310}}) needs the CamelContext route lock 
> ({{AbstractCamelContext.startRoute}}).
> * *Release.* {{ClusteredRoutePolicy.releaseClusterView}} is called from 
> {{onRemove}} of the last route of the policy and from {{doShutdown}}, so it 
> runs on every CamelContext stop and on every route removal or reload. It 
> takes the policy lock ({{:235}}) and, while it holds it, calls 
> {{removeEventListener}} ({{:239}}), which needs the write lock of the view 
> ({{AbstractCamelClusterView.java:104}}).
> * *removeRoute.* {{AbstractCamelContext.removeRoute}} holds the CamelContext 
> route lock while it shuts the route down and calls {{onRemove}}.
> {{ClusteredRoutePolicyFactory}} creates one policy per route, and each one 
> registers its own listener. A leadership event runs the listeners one after 
> the other, and each one starts its route inside the dispatch, with the read 
> lock of the view held. Two cycles follow:
> # *CamelContext stop* (two locks). The stop releases a policy further down 
> the list: it takes that policy's lock and waits for the write lock of the 
> view. The dispatch reaches that policy's listener and waits for the same 
> policy lock.
> # *removeRoute* (three locks). The operator holds the CamelContext route lock 
> and waits in {{releaseClusterView}} for the policy lock (or, once that is 
> lock-free, for the write lock of the view). The dispatch holds the read lock 
> of the view and the policy lock, and waits in {{startRoute}} for the 
> CamelContext route lock.
> This also applies to a single policy shared by several routes: it holds its 
> own lock across {{startRoute}}, while {{removeRoute}}, holding the 
> CamelContext route lock, needs the same lock.
> The window is a leadership change that is still starting routes, for example 
> a consumer that takes a while to connect to a broker. A rolling restart 
> produces exactly this: A stops, B takes over and starts its routes, and B is 
> then stopped or has a route reloaded.
> With the file cluster service the stuck thread is the leadership thread of 
> {{FileLockClusterService}}. It stops writing the heartbeat but keeps the file 
> lock, so no other node can become the leader until the JVM is killed (for 
> example by the orchestrator after its stop timeout).
> h3. Reproduction
> Two nodes share one {{FileLockClusterService}} root. Each registers 
> {{ClusteredRoutePolicyFactory.forNamespace("ns")}} and has two routes: 
> "slow", whose consumer takes 1.5 s to start, and "other". A is the leader. A 
> is stopped, so B takes the leadership and its view dispatches the event; the 
> policy of "slow" starts its route. 300 ms later B's CamelContext is stopped, 
> or "other" is removed.
> CamelContext stop, blocked in 3 of 3 runs:
> {noformat}
> CamelContext.stop() of B STILL BLOCKED after 10006 ms
>   thread 'FileLockClusterService-B' WAITING at 
> ClusteredRoutePolicy.setLeader:267
>       from 
> ClusteredRoutePolicy$CamelClusterLeadershipListener.leadershipChanged:376, 
> AbstractCamelClusterView.fireLeadershipChangedEvent
>   thread 'operator' WAITING at LockHelper.doWithWriteLock:81
>       from AbstractCamelClusterView.removeEventListener:104, 
> ClusteredRoutePolicy.releaseClusterView:239
> {noformat}
> removeRoute("other"), blocked:
> {noformat}
> removeRoute(other) STILL BLOCKED after 10001 ms
>   thread 'FileLockClusterService-B' WAITING at 
> AbstractCamelContext.startRoute:1318
>       from InternalRouteController.startRoute:124, 
> DefaultRouteController.startRoute:133
>   thread 'operator' WAITING at ClusteredRoutePolicy.releaseClusterView:235
>       from ReferenceCount.release:71, ClusteredRoutePolicy.onRemove:199
> {noformat}
> The removeRoute interleaving is racy in this harness. The unit test in the PR 
> makes both cases deterministic: a consumer whose start waits on a latch, and 
> a gate listener that holds the dispatch until the view is being released.
> h3. Proposed fix
> Making the release lock-free is not enough: with {{releaseClusterView}} 
> taking no policy lock, removeRoute still deadlocks, now between the write 
> lock of the view (operator, holding the CamelContext route lock) and the 
> CamelContext route lock (dispatch, holding the read lock of the view). The 
> routes must not be started or stopped on the thread that dispatches the 
> event. The proposed change to {{ClusteredRoutePolicy}}:
> * The leadership listener hands the change to the policy's own thread, which 
> starts or stops the routes under the policy lock. The view lock is then never 
> held while the CamelContext route lock is awaited, and a slow route start no 
> longer blocks the leadership thread of the cluster service (the file 
> service's heartbeat).
> * Threading: the thread comes from a pool created through the 
> {{ExecutorServiceManager}} (named and managed like the other Camel pools) 
> with no core thread, at most one thread and a keep-alive of one second. One 
> thread at most keeps the changes of a policy in order, and it exits when the 
> leadership does not change, so {{ClusteredRoutePolicyFactory}} (a policy per 
> route) does not keep a thread per route, and a removed route leaves no thread 
> behind. One shared thread per CamelContext was not used: it would need an 
> owner with its own lifecycle, and a slow route start would delay the changes 
> of every other policy.
> * The policy thread reads the leadership when it applies a change, so the 
> last change wins, and at most one change is queued. A change is never applied 
> on the dispatching thread: after the policy has been shut down it is 
> rejected, logged and ignored. Errors while applying a change are logged.
> * {{retainClusterView}} and {{releaseClusterView}} take no policy lock. The 
> view reference is swapped atomically, and the release only resets the leader 
> flag, as no route is left to stop. Retain and release are serialized by a 
> separate small lock that only the threads adding and removing routes take, so 
> a shared policy cannot release a view that a concurrent route addition has 
> just retained. The policy thread ignores a change for a view that has been 
> released in the meantime.
> * {{retainClusterView}} reads the current leadership right away, so 
> {{onInit}} still lets the route controller start a route added while the node 
> is the leader.
> * The deferred start after the CamelContext has started is checked under the 
> policy lock, as the policy thread may now apply the leadership while the 
> CamelContext is starting.
> * Leadership changes, taken or lost, that reach the policy thread while the 
> CamelContext is stopping are ignored, in line with CAMEL-24545. The 
> CamelContext is stopping all the routes, consumers first, and stopping a 
> route from the policy thread would run a second shutdown of it through the 
> same {{ShutdownStrategy}}.
> Behaviour change: the routes are started and stopped shortly after the 
> leadership event, on the policy thread, instead of inside it. Most cluster 
> services fire the event from their own threads, but some fire it on the 
> calling thread (when a view starts or a listener is added). Code that fires 
> the event itself (as some tests do) should wait for the route status. 
> {{isLeader}} (JMX) reflects the view right away, and the routes follow.
> An alternative is to iterate over a snapshot of the listeners in 
> {{AbstractCamelClusterView.doWithListener}}, so no view lock is held during 
> the callbacks. That changes every cluster view and camel-master, so it is 
> only mentioned as an option.
> A TLA+ model of the view lock and two policy locks shows the two-lock cycle. 
> It does not model the CamelContext route lock, so it does not cover the 
> removeRoute case.
> h3. Follow-up
> Once this is fixed, views could notify the listeners that are still 
> registered when a view is force-stopped (JMX {{stopView}}, or stop of the 
> cluster service). Today a leader whose view is force-stopped keeps its 
> clustered and master routes running while another node takes over (file and 
> ZooKeeper). This is left for a separate issue.
> Affected: long-standing. In 4.4.0 {{releaseClusterView}} and {{setLeader}} 
> were {{synchronized}} methods and the view already used a {{StampedLock}}, so 
> the lock order was the same; CAMEL-20199 (4.8.0) replaced the monitor with a 
> {{ReentrantLock}}. Main, 4.22.x, 4.18.x and 4.14.x are affected. The 
> ZooKeeper stop trigger is fixed in 4.18.5 and 4.22.1, but other ZooKeeper 
> events and all other cluster services still hit it.
> Duplicate check (2026-09-27): JIRA text "ClusteredRoutePolicy" returns 8 
> issues. CAMEL-24545 is the ZooKeeper-only fix of the same inversion, 
> CAMEL-24626 fixed the same pattern in camel-master, and CAMEL-17090 (2021) is 
> a different lock-up. GitHub PRs for "ClusteredRoutePolicy": #25842 touches 
> only {{ZooKeeperClusterView}}. No open PR changes {{ClusteredRoutePolicy}} or 
> {{AbstractCamelClusterView}}.
> _Filed with Claude Code on behalf of allthingssecurity._



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

Reply via email to