This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 6319543d332f CAMEL-25659: camel-sjms - recovery notices a connection
failure that happens before the consumers are created (#27856)
6319543d332f is described below
commit 6319543d332f8a8544085f1450d06decc82d22a4
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Oct 15 11:16:52 2026 +0530
CAMEL-25659: camel-sjms - recovery notices a connection failure that
happens before the consumers are created (#27856)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../consumer/SimpleMessageListenerContainer.java | 30 +++++++++++--
.../SjmsConsumerRecoveryFailedAttemptTest.java | 52 +++++++++++++++++++++-
2 files changed, 77 insertions(+), 5 deletions(-)
diff --git
a/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/consumer/SimpleMessageListenerContainer.java
b/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/consumer/SimpleMessageListenerContainer.java
index 09daa824b399..094fefff131f 100644
---
a/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/consumer/SimpleMessageListenerContainer.java
+++
b/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/consumer/SimpleMessageListenerContainer.java
@@ -22,6 +22,7 @@ import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -69,6 +70,9 @@ public class SimpleMessageListenerContainer extends
ServiceSupport
private final Lock consumerLock = new ReentrantLock();
private Set<MessageConsumer> consumers;
private Set<Session> sessions;
+ // the number of onException calls: a connection recovery compares it
before and after its attempt, as an
+ // onException that comes before the consumers of the new connection are
created finds none to drop
+ private final AtomicLong connectionFailures = new AtomicLong();
private ScheduledExecutorService recoverPool;
private BackgroundTask recoverTask;
private Future<?> recoverFuture;
@@ -180,6 +184,7 @@ public class SimpleMessageListenerContainer extends
ServiceSupport
@Override
public void onException(JMSException exception) {
+ connectionFailures.incrementAndGet();
if (exceptionListener != null) {
try {
exceptionListener.onException(exception);
@@ -208,13 +213,17 @@ public class SimpleMessageListenerContainer extends
ServiceSupport
protected boolean recoverConnection(BackgroundTask task) {
LOG.debug("Recovering from JMS Connection exception (attempt: {})",
task.iteration());
try {
- refreshConnection();
+ long failures = refreshConnectionAndCountFailures();
initConsumers();
connectionLock.lock();
try {
- if (consumers == null) {
- // the new connection failed while its consumers were
created: onException dropped them, and
- // did not schedule another recovery as this one is
running. So this one has to try again
+ if (consumers == null || connectionFailures.get() != failures)
{
+ // the new connection failed during this attempt:
onException dropped its consumers while they
+ // were created, or it came before and found none to drop
(then the consumers on the failed
+ // connection are dropped here). onException did not
schedule another recovery as this one is
+ // running. So this one has to try again
+ this.sessions = null;
+ this.consumers = null;
LOG.debug("JMS Connection failed again during recovery
(attempt: {}). Will try again in {} millis",
task.iteration(), endpoint.getRecoveryInterval());
return false;
@@ -455,14 +464,27 @@ public class SimpleMessageListenerContainer extends
ServiceSupport
}
protected final void refreshConnection() throws Exception {
+ refreshConnectionAndCountFailures();
+ }
+
+ /**
+ * Closes the connection and creates a new one, which is started if the
container is started.
+ *
+ * @return the number of connection failures (onException calls) before
the new connection was created, so a failure
+ * that is counted later may be one of the new connection
+ */
+ private long refreshConnectionAndCountFailures() throws Exception {
connectionLock.lock();
try {
closeConnection(connection);
this.connection = null;
+ // read after the old connection is closed and before the new one
has this container as exception listener
+ long failures = connectionFailures.get();
createConnection();
if (this.connectionStarted) {
startConnection();
}
+ return failures;
} finally {
connectionLock.unlock();
}
diff --git
a/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/consumer/SjmsConsumerRecoveryFailedAttemptTest.java
b/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/consumer/SjmsConsumerRecoveryFailedAttemptTest.java
index f009447343f8..71d9cc44e453 100644
---
a/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/consumer/SjmsConsumerRecoveryFailedAttemptTest.java
+++
b/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/consumer/SjmsConsumerRecoveryFailedAttemptTest.java
@@ -27,6 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import jakarta.jms.Connection;
import jakarta.jms.ConnectionFactory;
+import jakarta.jms.ExceptionListener;
import jakarta.jms.JMSContext;
import jakarta.jms.JMSException;
import jakarta.jms.Session;
@@ -57,6 +58,8 @@ class SjmsConsumerRecoveryFailedAttemptTest extends
JmsTestSupport {
private final List<TrackedConnection> connections = new
CopyOnWriteArrayList<>();
// what the next connection of the tracking connection factory does wrong
(taken by that connection)
private volatile Fault nextFault;
+ // whether the next connection of the tracking connection factory fails
when it is started (taken by it)
+ private volatile boolean nextFailsOnStart;
@BindToRegistry("trackingConnectionFactory")
private final ConnectionFactory trackingConnectionFactory = new
ConnectionFactory() {
@@ -163,6 +166,38 @@ class SjmsConsumerRecoveryFailedAttemptTest extends
JmsTestSupport {
assertEquals(3, connections.size());
}
+ /**
+ * The new connection reports its failure to the container (its exception
listener) when the recovery starts it,
+ * that is before the consumers are created, and it delivers no message;
the JMS provider still creates the sessions
+ * and consumers on it. The recovery has to notice the failure and try
again with another connection, although
+ * onException found no consumers to drop and none of the create calls
failed.
+ */
+ @Test
+ void testRecoveryTriesAgainWhenConnectionFailsBeforeConsumersAreCreated()
throws Exception {
+ context.getRouteController().startRoute("beforeconsumers");
+ MockEndpoint mock = getMockEndpoint("mock:beforeconsumers");
+ mock.expectedBodiesReceived("before");
+ template.sendBody(QUEUE + "beforeconsumers", "before");
+ mock.assertIsSatisfied();
+
+ nextFailsOnStart = true;
+ container("beforeconsumers").onException(new JMSException("Simulated
failure of the connection"));
+ await().atMost(20, TimeUnit.SECONDS).until(() -> connections.size() >=
2 && connections.get(1).failed.get());
+
+ // the first, the one that failed when it was started, the one in use
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() ->
assertEquals(3, connections.size(),
+ "connections: the recovery must try again, the connection
failed before the consumers were created"));
+ await().atMost(20, TimeUnit.SECONDS)
+ .untilAsserted(() -> assertEquals(CONSUMERS,
last().openConsumers(), "consumers on the new connection"));
+ assertTrue(connections.get(1).closed.get(), "the connection that
failed when it was started is closed");
+
+ mock.reset();
+ mock.expectedBodiesReceived("after");
+ template.sendBody(QUEUE + "beforeconsumers", "after");
+ mock.assertIsSatisfied();
+ assertEquals(3, connections.size());
+ }
+
/**
* A listener container that fails to start (the second consumer cannot be
created) closes the session and consumer
* it did create, and the session whose consumer failed. The connection is
kept for the next start.
@@ -207,7 +242,7 @@ class SjmsConsumerRecoveryFailedAttemptTest extends
JmsTestSupport {
@Override
public void configure() {
// each test starts its own route, so that the tracked
connections are the ones of that route
- for (String routeId : List.of("first", "last", "during")) {
+ for (String routeId : List.of("first", "last", "during",
"beforeconsumers")) {
from(QUEUE + routeId + "?concurrentConsumers=" + CONSUMERS
+ "&recoveryInterval=200"
+ "&connectionFactory=#trackingConnectionFactory")
.routeId(routeId).autoStartup(false).to("mock:" +
routeId);
@@ -223,9 +258,21 @@ class SjmsConsumerRecoveryFailedAttemptTest extends
JmsTestSupport {
TrackedConnection tracked = new TrackedConnection();
tracked.fault = nextFault;
nextFault = null;
+ tracked.failsOnStart = nextFailsOnStart;
+ nextFailsOnStart = false;
connections.add(tracked);
return (Connection)
Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[] {
Connection.class },
(proxy, method, args) -> {
+ if ("setExceptionListener".equals(method.getName())) {
+ tracked.exceptionListener = (ExceptionListener)
args[0];
+ }
+ if ("start".equals(method.getName()) &&
tracked.failsOnStart) {
+ // the connection is not started, so its consumers get
no message, and the failure is reported
+ // on the thread that starts it
+ tracked.failed.set(true);
+ tracked.exceptionListener.onException(new
JMSException("Simulated: the connection failed"));
+ return null;
+ }
if ("createSession".equals(method.getName())) {
Fault fault = tracked.fault;
int number = tracked.sessionCalls.incrementAndGet();
@@ -293,7 +340,10 @@ class SjmsConsumerRecoveryFailedAttemptTest extends
JmsTestSupport {
final AtomicInteger sessionCalls = new AtomicInteger();
final List<TrackedSession> sessions = new CopyOnWriteArrayList<>();
final AtomicBoolean closed = new AtomicBoolean();
+ final AtomicBoolean failed = new AtomicBoolean();
volatile Fault fault;
+ volatile boolean failsOnStart;
+ volatile ExceptionListener exceptionListener;
// a closed connection has closed its sessions, and a closed session
its consumer
int openSessions() {