shashank created CAMEL-25052:
--------------------------------
Summary: camel-file - FileLockClusterView: stopping the view no
longer ends a leadership check that is already running (regression from
CAMEL-22784), so a stopped view can take the cluster lock, and a quick restart
doubles the checks
Key: CAMEL-25052
URL: https://issues.apache.org/jira/browse/CAMEL-25052
Project: Camel
Issue Type: Bug
Components: camel-file
Reporter: shashank
Since CAMEL-22784 ("Use predictable scheduling for FileLockClusterService lock
acquisition", PR #20686, commit 7ee5afcd2f), {{FileLockClusterView}} runs its
leadership check as a chain of one-shot tasks: {{scheduleTryLock}}
({{FileLockClusterView.java:296-324}}) calls {{executor.schedule(this::tryLock,
...)}}, and {{tryLock}} schedules the next run in its {{finally}} block
({{:280}}). The {{ScheduledFuture}} is no longer stored in the {{task}} field
({{:60}}). {{task}} therefore stays null, and {{closeInternal()}}
({{:156-163}}), which {{doStop}} ({{:133-154}}) calls, cancels nothing. Before
that commit, {{task.cancel(true)}} interrupted a running check.
{{tryLock}} checks {{isStarting() || isStarted()}} only once, when it begins
({{:196}}). This has two consequences.
# *A stop during a running check.* A follower that sees a stale or absent
leader opens the lock and data files and calls {{FileChannel.tryLock}}
({{:250-263}}). If the view is stopped meanwhile, the check carries on after
{{doStop}} has closed the files. It takes the file lock, sets the member to
LEADER, fires the leadership event and writes its heartbeat once, on a view
that is Stopped. The next run of the chain sees the view stopped and ends. The
stopped node reports {{isLeader(namespace)=true}}, and the OS lock stays held
until the view is started again ({{doStart}} closes leftover files at
{{:101-104}}) or the JVM exits. Meanwhile no other node can become the leader,
so the clustered routes run nowhere.
# *A stop and start within one {{acquireLockInterval}}.* The chain of the
previous start is still scheduled, sees the view started again and keeps
running next to the chain of the new start. Every quick restart adds one more
chain.
The first case needs the view to be stopped while its node is inside the
acquisition I/O (it has just seen the old leader go away), and the JVM to keep
running afterwards. The view is stopped when its last user releases it: a
camel-master route stops, the last route with a {{ClusteredRoutePolicy}} is
removed, or the CamelContext stops in a JVM that keeps running (an application
server, a Spring context refresh). JMX {{stopView}} also stops it. All of these
can coincide with a leadership handover. The window is the acquisition I/O:
small on a local disk, larger on the network storage this service targets
({{clusterDataTaskTimeout}} defaults to 10 s, with 5 attempts per task).
h3. Reproduction
Each node is a CamelContext with its own {{FileLockClusterService}} on a shared
root ({{acquireLockInterval}} 500 ms) and a {{master:}} route.
* A is the leader. B follows. A's context stops, and B's next check is held
(test hook in {{createRandomAccessFile}}) just before it opens the lock file.
B's master route is stopped, which stops B's view, then the hold is released.
Result: B's view is Stopped but {{isLeader(ns)=true}}. A probe from a separate
JVM finds the lock held, and a node C in a separate JVM does not become the
leader within 6 s.
* The unit test in the PR does the same in one JVM, with B's view released
directly: B reports leadership and the lock file stays locked by B's channel.
* Without a hook, with 200 ms file opens (as on network storage) and B's master
route stopped at a random time during the takeover: 3 of 15 rounds ended with
the stopped view holding the lock. With local file opens: 0 of 15.
* One node, three quick {{stopRoute}}/{{startRoute}} of its master route: the
leadership check runs 10 times per 5 s before and 40 times per 5 s after.
A TLA+ model of two or three nodes (the check, view stop and start, the OS
lock, and the camel-master listener) finds the same traces: {{NoStoppedHolder}}
and {{StoppedNotLeader}} are violated by the trace TickAcquire(a),
ConsumerStop(a), Acquire(a); {{EventuallyLeader}} is violated; {{OneChain}} is
violated by a quick stop and start. The fixed model holds all properties.
h3. Proposed fix
* Give every start a generation number, incremented on every start and stop
under a small state lock. A check ends without rescheduling itself once its
generation is over, which ends the chain on stop and keeps one chain per start.
This replaces the dead {{task}} field.
* Open the files and take the OS lock into local variables, without holding any
lock. Then re-check the generation under the state lock and only then publish
the lock and files to the view and set LEADER. If the view was stopped
meanwhile, release the lock and close the files instead.
* {{doStop}} increments the generation and takes over the lock and files under
the same state lock, then does the file I/O (truncate, release, close) outside
it.
* The leadership event is fired outside the state lock, as before. The state
lock is never held during file I/O or listener callbacks, so a stop does not
wait for a slow (NFS) check.
Affected: 4.17.0 and later, and 4.14.5 and later (the backport PR #20780
brought the change to 4.14.x). Checked at the release tags: the {{task =}}
assignment is present in 4.16.0 and 4.14.4, and absent in 4.14.5, 4.17.0,
4.18.0, 4.18.4, 4.22.0 and main.
Duplicate check (2026-09-27): JIRA text "FileLockClusterView" (6 issues),
"FileLockClusterService" (4) and "file lock cluster" (15) return only the
CAMEL-22430 and CAMEL-22784 work and CAMEL-22541 (a flaky test). GitHub PRs for
these names are the CAMEL-22784 series (#20433, #20452, #20526, #20578, #20590,
#20686) and #20780. Nothing covers the stop.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)