shashank created CAMEL-25612:
--------------------------------

             Summary: camel-sjms - when a session or consumer cannot be created 
during connection recovery, the next attempt reports success without creating 
any consumer: the route stays started and receives nothing
                 Key: CAMEL-25612
                 URL: https://issues.apache.org/jira/browse/CAMEL-25612
             Project: Camel
          Issue Type: Bug
          Components: camel-sjms
            Reporter: shashank
            Assignee: shashank


{{SimpleMessageListenerContainer.initConsumers}} (lines 355-374 at main 
a7169d7c6ab6) assigns the two sets before it fills them:
{code:java}
if (consumers == null) {
    sessions = new HashSet<>(concurrentConsumers);
    consumers = new HashSet<>(concurrentConsumers);
    for (int i = 0; i < this.concurrentConsumers; i++) {
        Session session = createSession(connection, endpoint);
        MessageConsumer consumer = createMessageConsumer(session);
        configureConsumer(consumer, session);
        sessions.add(session);
        consumers.add(consumer);
    }
}
{code}
When {{createSession}} or {{createMessageConsumer}} throws, the sets stay: 
empty, or with the consumers created so far.

During connection recovery this is permanent. {{onException}} (lines 198-204) 
sets both fields to {{null}} and schedules the recovery task; 
{{recoverConnection}} (lines 208-233) calls {{refreshConnection()}} and 
{{initConsumers()}}:
# attempt n: the new connection is created, {{initConsumers}} assigns the sets, 
one of the create calls fails, the attempt fails ("Failed to recover JMS 
Connection (attempt: n)", WARN) and is repeated after {{recoveryInterval}};
# attempt n+1: {{refreshConnection}} closes that connection (and with it the 
sessions of attempt n) and creates another one; {{initConsumers}} finds 
{{consumers != null}} and creates nothing; {{recoverConnection}} sets 
{{recoverTask = null}} and returns {{true}} ("Successfully recovered JMS 
Connection", DEBUG).

The container now has a started connection without any consumer. The route is 
started, nothing above DEBUG is logged after the WARN of attempt n, no further 
recovery is scheduled, and no message is consumed until the route or the 
context is restarted. The same container is the reply listener of the InOut 
producer ({{QueueReplyManager}}, {{TemporaryQueueReplyManager}}): there the 
replies are then not received (read in the code, not tested).

When does a create call fail on a connection that was just created:
* the connection fails again between {{refreshConnection}} and the end of the 
loop (for example a broker that is still restarting; not reproduced with a real 
broker). The JMS provider then also calls {{onException}}, which sets the 
fields to {{null}} again; the defect needs that call to arrive before 
{{initConsumers}} assigns the sets (it is a race with a narrow window);
* the connection is fine and the destination or the permission is not there yet 
(for example a broker that accepts connections before its queues are deployed, 
or an authorization that is loaded later; not reproduced with a real broker, 
the test makes the create call fail). No {{onException}} follows, so every such 
failure ends in the state above.

h3. Reproduction

New {{SjmsConsumerRecoveryFailedAttemptTest}} with the embedded Artemis broker 
of the module and a connection factory that tracks the connections, sessions 
and consumers of the route and can make one create call fail 
({{concurrentConsumers=3}}, {{recoveryInterval=200}}). After a simulated 
connection failure the first session (one test) or the last session (another 
test) of the next connection cannot be created; the test waits for the 
following attempt (the third connection) and sends a message. On main both 
tests fail with {{mock://first Received message count. Expected: <1> but was: 
<0>}} ({{mock://last}} for the other one): the third connection has no consumer.

h3. Proposed fix

{{initConsumers}} closes what it created and sets the two fields back to 
{{null}} when the loop fails, so the next attempt creates all the consumers.

Two related things in the same two methods:
* When the listener container fails to start because a consumer cannot be 
created, the session of that consumer stayed open on the connection, which is 
kept for the next start (the sessions that got their consumer are closed by 
{{stopConsumers}}). The same {{catch}} block closes it. A third test covers it; 
on main it fails with {{open sessions after the failed start ==> expected: <0> 
but was: <1>}}.
* {{onException}} does not schedule a recovery while one is running 
({{scheduleConnectionRecovery}}, line 242). When it drops the consumers while 
the running attempt creates them, the attempt has to run again. On main this 
mostly happened by accident: the loop added to the fields that {{onException}} 
had set to {{null}}, and the {{NullPointerException}} failed the attempt (only 
an {{onException}} after the last pass of the loop was lost). The loop no 
longer fails that way, so {{recoverConnection}} now returns {{false}} (run 
again after {{recoveryInterval}}) when the consumers are {{null}} after 
{{initConsumers}}. A fourth test covers this; it passes on main too, for the 
reason above.

No retry loop is added: the attempts are the ones of the existing recovery 
task, {{recoveryInterval}} apart, with one WARN line (no stack trace) per 
failed attempt as before.

camel-sjms tests with the fix: 138 run, 0 failures, 5 skipped. camel-sjms2 (it 
uses the same container) was not run.

How it was found: a small TLA+ model of the container (connection alive or 
failed, the pending {{onException}} call of the provider, the {{consumers}} 
field, the consumers that exist on the current connection, the steps of one 
recovery attempt). For main TLC reports a state where no recovery is scheduled, 
the connection is alive and it has no consumer: after two connection failures 
(11 states), and after one connection failure and one create call that fails on 
a live connection (9 states). With the fix the invariant holds, and "eventually 
all consumers exist for good" holds for 3 consumers, 3 connection failures and 
2 failing create calls (540 states). The model is a sketch of the code and does 
not cover suspend / resume or the stop of the container; the test above is the 
evidence.

Affected: main (a7169d7c6ab6; tested). camel-4.23.x, camel-4.22.x and 
camel-4.18.x have the same loop in {{initConsumers}} and the same 
{{recoverConnection}} / {{onException}} (read, not run). On camel-4.14.x the 
loop is the same, but {{recoverConnection}} is different (it returns {{false}}; 
the fix of CAMEL-23646 is not there); not checked further.

Overlap: the open pull request apache/camel#27173 (CAMEL-16039) changes 
{{onException}} in the same file (it moves the three lines that reset the 
fields into a method). It does not touch {{initConsumers}} or 
{{recoverConnection}}; the two changes merge without a conflict today.

Duplicate check (2026-10-13): JIRA text "sjms initConsumers" (CAMEL-25312 and 
CAMEL-21705, neither about a failed attempt), "sjms recover connection 
consumers" (none), "SimpleMessageListenerContainer" in camel-sjms (CAMEL-25312, 
CAMEL-24626, CAMEL-22026, CAMEL-16039); GitHub pull requests "initConsumers 
sjms": #27388, #23639, #23641 (the return value of the task), none for this.

_Filed with Claude Code on behalf of allthingssecurity._




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

Reply via email to