shashank created CAMEL-26536:
--------------------------------
Summary: camel-jms - with asyncStopListener=true, a JmsConsumer
stopped and started again ends up started without a running listener container
Key: CAMEL-26536
URL: https://issues.apache.org/jira/browse/CAMEL-26536
Project: Camel
Issue Type: Bug
Components: camel-jms
Reporter: shashank
Assignee: shashank
Found with a TLA+ model of {{JmsConsumer}} with {{asyncStartListener}} /
{{asyncStopListener}} (the lifecycle thread, the asynchronous stop task and the
asynchronous start task on the component's pool), cross-checked with a SPIN
model, then reproduced on the real classes.
With {{asyncStopListener=true}}, {{JmsConsumer.doStop}} submits
{{stopAndDestroyListenerContainer()}} to the component's thread pool and
returns, with {{listenerContainer}} still set. If the same consumer is started
again before that task has run (route policies that stop and start the
consumer, such as the leader-election policies {{HazelcastRoutePolicy}} and
{{MasterRoutePolicy}} of camel-zookeeper-master, the JMX start/stop operations
of the consumer, or code that calls {{stop()}} / {{start()}} on it),
{{doStart}} sees the old listener container, does not create a new one, and
initializes and starts it again. The stop task then reads the field, stops and
destroys that container, and sets {{listenerContainer}} to null. The consumer
reports Started but has no listener container and consumes nothing, with no
error logged. If the stop task nulls the field while {{doStart}} is between its
null check and {{afterPropertiesSet()}}, the start fails with a
NullPointerException instead. The stop task also shuts down the private thread
pool stored in {{executorService}}, which can be the pool of a listener
container created by the new start.
Short trace of the model:
{noformat}
1 lifecycle : doStop -> listenerContainer = C1, submit stop task
(JmsConsumer:216-219)
2 lifecycle : doStart -> listenerContainer != null, keep C1
(:149)
3 lifecycle : C1.afterPropertiesSet(); C1.start(); initialized = true
(:177-189)
4 stop task : listenerContainer.stop(); listenerContainer.destroy()
(:195-196) -> C1 not running
5 stop task : listenerContainer = null; initialized = false
(:203-205)
-> consumer Started, no listener container
{noformat}
With {{asyncStartListener=true}} as well, the start task works on whatever the
field holds when it runs: it can start the container the stop task destroys,
hit the null field (NPE logged as "ignored"), or initialize a newer container a
second time.
The real-class test holds the tasks the consumer submits to the pool and runs
them in a chosen order: stopping and starting the consumer with the stop task
pending leaves {{getListenerContainer().isRunning()}} false and the next
message is not consumed. Without hooks, 30 stop/start cycles of the consumer
with the real pool (in-VM Artemis) ended 30 times without a running listener
container (27 started without a listener, 3 starts failed with the NPE).
A route restart ({{restartRoute}}, {{stopRoute}} + {{startRoute}}) is not
affected, as the route creates a new consumer when it starts again.
*Proposal:* {{doStop}} detaches the listener container under a lock of the
consumer (the container, its private thread pool and the {{initialized}} flag
are taken and cleared at once), and the stop task stops and destroys only the
container (and pool) it was given, so a start that runs before the stop task
creates a new listener container. {{doStart}} hands the container it created to
the start task, which does nothing if the consumer no longer uses it, and after
starting it checks again under the lock and stops and destroys it if the
consumer was stopped meanwhile.
Affected: main, 4.23.x, 4.22.x (the same {{JmsConsumer}} code), 4.18.x, 4.14.x
(the same bug; {{initialized}} is set in {{doStart}} there).
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)