This is an automated email from the ASF dual-hosted git repository.
fmariani 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 dfd1f9a03f5a CAMEL-23194: Fix JMS shutdown deadlock that hangs CI
builds
dfd1f9a03f5a is described below
commit dfd1f9a03f5a67929c5da583d33533e4e128a106
Author: Croway <[email protected]>
AuthorDate: Wed Apr 1 16:22:22 2026 +0200
CAMEL-23194: Fix JMS shutdown deadlock that hangs CI builds
Root cause: deadlock between BaseService.lock and Spring's
lifecycleMonitor during CamelContext shutdown in afterAll.
Thread 1 (afterAll -> context.stop()):
AbstractCamelContext.doStop()
-> JmsProducer.doStop()
-> ReplyManagerSupport.doStop()
-> DefaultMessageListenerContainer.doShutdown()
-> Object.wait() <- blocked, waiting for listener thread
Thread 2 (QueueReplyManager listener):
DestinationResolverDelegate.resolveDestinationName()
-> BaseService.lock <- blocked, held by Thread 1
Introduced by CAMEL-20199 (Oct 2024) which migrated synchronized
blocks to ReentrantLock. Before: BaseService.stop() used
synchronized(lock) on a private Object, while the inner class used
synchronized(QueueReplyManager.this) on the instance monitor — two
independent locks. After: both became QueueReplyManager.this.lock
(the same inherited ReentrantLock), creating the circular wait.
Fix: DestinationResolverDelegate now uses a dedicated ReentrantLock
instead of BaseService.lock, restoring the original two-lock design.
Added volatile + fast-path for the destination field.
Applied to both camel-jms and camel-sjms which share the same
pattern via their respective QueueReplyManager classes.
---
.../camel/component/jms/reply/QueueReplyManager.java | 19 ++++++++++++++++---
.../camel/component/sjms/reply/QueueReplyManager.java | 17 ++++++++++++++---
2 files changed, 30 insertions(+), 6 deletions(-)
diff --git
a/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/QueueReplyManager.java
b/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/QueueReplyManager.java
index 745ea41773d6..569d9864cd8c 100644
---
a/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/QueueReplyManager.java
+++
b/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/QueueReplyManager.java
@@ -18,6 +18,8 @@ package org.apache.camel.component.jms.reply;
import java.math.BigInteger;
import java.util.Random;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import jakarta.jms.Destination;
import jakarta.jms.JMSException;
@@ -98,7 +100,13 @@ public class QueueReplyManager extends ReplyManagerSupport {
private final class DestinationResolverDelegate implements
DestinationResolver {
private final DestinationResolver delegate;
- private Destination destination;
+ // Use a dedicated lock instead of BaseService.lock to avoid deadlock
+ // during shutdown: BaseService.stop() holds its lock while calling
+ // doStop() -> listenerContainer.destroy() -> doShutdown() which waits
+ // for listener threads to finish. If a listener thread needs to
resolve
+ // the destination, it would deadlock trying to acquire
BaseService.lock.
+ private final Lock destinationLock = new ReentrantLock();
+ private volatile Destination destination;
DestinationResolverDelegate(DestinationResolver delegate) {
this.delegate = delegate;
@@ -109,7 +117,12 @@ public class QueueReplyManager extends ReplyManagerSupport
{
Session session, String destinationName,
boolean pubSubDomain)
throws JMSException {
- QueueReplyManager.this.lock.lock();
+ // fast path: destination already resolved
+ Destination answer = destination;
+ if (answer != null) {
+ return answer;
+ }
+ destinationLock.lock();
try {
// resolve the reply to destination
if (destination == null) {
@@ -117,7 +130,7 @@ public class QueueReplyManager extends ReplyManagerSupport {
setReplyTo(destination);
}
} finally {
- QueueReplyManager.this.lock.unlock();
+ destinationLock.unlock();
}
return destination;
}
diff --git
a/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/QueueReplyManager.java
b/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/QueueReplyManager.java
index c92ba21e5161..0daa079ef0f6 100644
---
a/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/QueueReplyManager.java
+++
b/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/QueueReplyManager.java
@@ -16,6 +16,9 @@
*/
package org.apache.camel.component.sjms.reply;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
import jakarta.jms.Destination;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
@@ -65,7 +68,10 @@ public class QueueReplyManager extends ReplyManagerSupport {
private final class DestinationResolverDelegate implements
DestinationCreationStrategy {
private final DestinationCreationStrategy delegate;
- private Destination destination;
+ // Use a dedicated lock instead of BaseService.lock to avoid deadlock
+ // during shutdown (same issue as CAMEL-23194 in camel-jms)
+ private final Lock destinationLock = new ReentrantLock();
+ private volatile Destination destination;
DestinationResolverDelegate(DestinationCreationStrategy delegate) {
this.delegate = delegate;
@@ -73,7 +79,12 @@ public class QueueReplyManager extends ReplyManagerSupport {
@Override
public Destination createDestination(Session session, String
destinationName, boolean topic) throws JMSException {
- QueueReplyManager.this.lock.lock();
+ // fast path: destination already resolved
+ Destination answer = destination;
+ if (answer != null) {
+ return answer;
+ }
+ destinationLock.lock();
try {
// resolve the reply to destination
if (destination == null) {
@@ -81,7 +92,7 @@ public class QueueReplyManager extends ReplyManagerSupport {
setReplyTo(destination);
}
} finally {
- QueueReplyManager.this.lock.unlock();
+ destinationLock.unlock();
}
return destination;
}