shashank created CAMEL-25312:
--------------------------------

             Summary: camel-sjms - a suspended consumer keeps consuming 
(graceful shutdown, suspendRoute, route policies), and a consumer started again 
receives nothing
                 Key: CAMEL-25312
                 URL: https://issues.apache.org/jira/browse/CAMEL-25312
             Project: Camel
          Issue Type: Bug
          Components: camel-sjms, camel-sjms2
            Reporter: shashank


{{SjmsConsumer}} implements {{Suspendable}} but has no 
{{doSuspend}}/{{doResume}}, so {{suspend()}} only changes its status: the 
listener container keeps receiving and routing messages while the route is 
reported as suspended. Everything that suspends a consumer is affected:

* the graceful shutdown (default): {{DefaultShutdownStrategy}} suspends a 
{{Suspendable}} consumer instead of stopping it, defers its shutdown and waits 
until no exchange is inflight. The sjms consumer keeps taking new messages 
meanwhile, so with a steady flow the route (or the CamelContext) only stops at 
the shutdown timeout, and the exchanges inflight then are forced. This is what 
CAMEL-9567 fixed in 2016 for the old consumer.
* {{suspendRoute}} (route controller, JMX) and the route policies that suspend 
the consumer ({{ThrottlingInflightRoutePolicy}}, 
{{ThrottlingExceptionRoutePolicy}} with an open circuit, ...): nothing is 
throttled, an open circuit keeps routing (failing) messages.

Separately, {{SimpleMessageListenerContainer.stopConsumers}} closes the 
consumers and sessions but keeps them in the {{consumers}}/{{sessions}} fields, 
and {{initConsumers}} only creates consumers when {{consumers == null}}. A 
container that is started again therefore keeps its closed consumers and never 
receives a message: this happens when the same consumer instance is stopped and 
started (the stop/start operations of the managed consumer in JMX; a route 
restart creates a new consumer and is not affected).

h3. Reproduction

{{SjmsConsumerRestartTest}} (camel-sjms, embedded Artemis), on main:

{noformat}
testSuspendResumeRoute: suspendRoute("suspend"), send a message
  mock://suspend Received message count. Expected: <0> but was: <1>
testSuspendResumeConsumer: ServiceHelper.suspendService(consumer) as the route 
policies do
  mock://policy Received message count. Expected: <0> but was: <1>
testGracefulStopTakesNoNewMessage: message A blocks in the route, stopRoute 
(graceful) begins and suspends
  the consumer, the test waits for the connection to stop, then sends message B 
and releases A
  ConditionTimeout: the connection is never stopped within 20 seconds
  (before the waits for the stop thread were added: A message sent after the 
route began to stop must not be
  consumed ==> expected: <1> but was: <2>)
testStopStartConsumer: the consumer instance stopped and started twice, two 
messages sent
  mock://restart Received message count. Expected: <2> but was: <0>
{noformat}

{{SjmsConsumerRoutePolicyTest}} (the route policy suspends the consumer when an 
exchange is done), on main:

{noformat}
testCircuitBreakerSuspendsConsumer: a failure opens the circuit 
(ThrottlingExceptionRoutePolicy) on the listener thread
  mock://result Received message count. Expected: <0> but was: <1>
testSuspendWhileThePolicySuspends: suspendService(consumer) while the exchange 
is in flight, then the policy opens the circuit
  mock://blocking Received message count. Expected: <0> but was: <1>
testCircuitBreakerOnAnotherThread: the same with the route continued by 
threads(1)
  mock://threads Received message count. Expected: <0> but was: <1>
{noformat}

(These three tests and the waits of {{SjmsConsumerRestartTest}} use two 
package-private hooks of the container ({{isSuspendResumePending}}, 
{{isConnectionStarted}}) and a getter on the consumer 
({{getListenerContainer}}), so on main they ran with those three methods 
added.) The control {{testStopStartRoute}}, a route restart, passes; 
{{testGracefulStopAsyncReply}} guards the fix: an {{asyncConsumer}} InOut 
exchange in flight during the graceful stop must still send its reply with the 
consumer's session. No sleeps: latches, {{MockEndpoint}} assert period, 
Awaitility.

The defect was found with a TLA+ model of the consumer and its listener 
container (route start/stop/suspend/resume of one consumer instance, the 
connection recovery task, each Java statement one step): "a suspended route 
receives nothing" is violated in 5 steps (start, suspend), and "a started route 
receives messages" in 13 steps for start, stop, start (the closed consumers are 
kept). With both parts of the fix all properties hold, also with connection 
failures and recovery during the operations; the model also showed that 
{{doShutdown}} must forget the consumers too (the recovery may create consumers 
after {{stopConsumers}}). That model makes the suspend one atomic step on a 
management thread; a second model of the threads (message listener, route 
policy, management thread, the container's stop/start thread, a {{threads()}} 
thread; the JMS rule that the listener cannot stop its connection and that 
{{Connection.stop()}} waits for it; the consumer lock) shows the ignored stop 
on the listener thread and the two waiting cycles of a suspend on the calling 
thread, and that a single ordered stop/start thread has none (TLC deadlock 
check on; a mutation that starts the connection on the calling thread while a 
stop is queued is caught).

h3. Proposed fix

* {{SjmsConsumer.doSuspend}}/{{doResume}} suspend/resume the listener 
container, whose {{doSuspend}} stops the connection ({{Connection.stop()}}: no 
message is delivered any more) and {{doResume}} starts it again. The consumers 
and sessions stay open, as exchanges in flight may still use them (an 
{{asyncConsumer}} sends the reply with the consumer's session when the exchange 
is done); closing them on suspend breaks that, which the guard test shows.
* The connection is stopped and started by a single thread of the container 
({{SjmsSuspendResume}}), in the order of the suspend and resume calls, and 
suspend/resume do not wait for it. {{Connection.stop()}} waits for the message 
listeners in progress and must not be called by a message listener of the 
connection (JMS 2.0; Artemis throws {{IllegalStateException}}), while a route 
policy suspends the consumer when an exchange is done: for a synchronous 
consumer on the listener thread itself (a suspend on the calling thread did 
nothing there: the exception was ignored and the consumer kept receiving), or 
on a thread the listener waits for (route continued with {{threads()}}), and a 
graceful shutdown or JMX suspend waiting in {{Connection.stop()}} would hold 
the lock of the consumer that the listener's route policy needs. Both cycles 
waited until Artemis gave up after its {{onMessageCloseTimeout}} (10 s, 
"AMQ212002: Timed out after waiting 10000ms for handler to complete 
processing"). {{doStop}} lets a pending stop/start finish first.
* {{SimpleMessageListenerContainer.stopConsumers}} sets {{consumers}} and 
{{sessions}} to null after closing them; {{doShutdown}} does the same after 
closing the connection (the connection recovery may have created consumers 
after {{stopConsumers}}; the model found this).

Limits: messages the JMS client has prefetched stay with the stopped connection 
until it is started again or closed. A connection factory that hands out a 
shared connection and does not stop it ({{JmsPoolConnectionFactory}} of 
pooled-jms: {{stop()}} is a no-op; Spring's {{SingleConnectionFactory}} only 
stops the shared connection when the last user stops it) keeps delivering to a 
suspended consumer, as today. Stopping the connection of one consumer never 
suspends another route: each listener container creates its own connection, and 
the pooling factories above do not stop a shared one.

Upgrade guide note (next to the other camel-sjms note): a suspended sjms route 
now stops consuming, and the limits above.

With the fix the new tests and the camel-sjms (132 tests, 5 skipped) and 
camel-sjms2 (23 tests) suites pass; {{MllpTcpServerConsumerTransactionTest}} 
(camel-mllp, uses sjms) is disabled ({{@Disabled}}, 2 skipped).

Affected: 4.14.x, 4.18.x and main (same code, GitHub contents API), since 3.8.0.

Duplicate check (2026-10-04): JIRA text "sjms" with "suspend" (CAMEL-9567 only, 
the old consumer), "sjms" with restart/stopRoute/startRoute (none), 
"SimpleMessageListenerContainer" (CAMEL-22026, thread leak on stop, fixed by 
closing the connection on shutdown; not this), sjms issues since 2024 (10, 
none). GitHub pull requests "sjms suspend" (none); open PR #27173 (batch 
consumer) changes {{SimpleMessageListenerContainer}} in other places.

_Filed with Claude Code on behalf of allthingssecurity._




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

Reply via email to