You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/07/15 13:58:29 UTC

(camel) branch fix/CAMEL-24073 created (now a6e0dcff9e27)

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a change to branch fix/CAMEL-24073
in repository https://gitbox.apache.org/repos/asf/camel.git


      at a6e0dcff9e27 CAMEL-24073: camel-jms - Cancel reply correlation on send failure to prevent double callback

This branch includes the following new commits:

     new a6e0dcff9e27 CAMEL-24073: camel-jms - Cancel reply correlation on send failure to prevent double callback

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 01/01: CAMEL-24073: camel-jms - Cancel reply correlation on send failure to prevent double callback

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-24073
in repository https://gitbox.apache.org/repos/asf/camel.git

commit a6e0dcff9e273fba92a9de679cee041095589597
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 15 15:58:12 2026 +0200

    CAMEL-24073: camel-jms - Cancel reply correlation on send failure to prevent double callback
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
 .../apache/camel/component/jms/JmsProducer.java    |  11 +-
 .../camel/component/jms/reply/ReplyManager.java    |  10 ++
 .../component/jms/reply/ReplyManagerSupport.java   |  10 ++
 .../jms/JmsInOutSendFailureCallbackTest.java       | 150 +++++++++++++++++++++
 4 files changed, 180 insertions(+), 1 deletion(-)

diff --git a/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsProducer.java b/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsProducer.java
index 0db06aba2d33..f3b2ddf36cc2 100644
--- a/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsProducer.java
+++ b/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsProducer.java
@@ -221,6 +221,8 @@ public class JmsProducer extends DefaultAsyncProducer {
             in.setHeader(correlationPropertyToUse, GENERATED_CORRELATION_ID_PREFIX + getUuidGenerator().generateUuid());
         }
 
+        final String[] registeredCorrelationId = new String[1];
+
         MessageCreator messageCreator = new MessageCreator() {
             public Message createMessage(Session session) throws JMSException {
                 Message answer = endpoint.getBinding().makeJmsMessage(exchange, in, session, null);
@@ -241,6 +243,7 @@ public class JmsProducer extends DefaultAsyncProducer {
 
                 String correlationId = determineCorrelationId(answer, provisionalCorrelationId);
                 replyManager.registerReply(replyManager, exchange, callback, originalCorrelationId, correlationId, timeout);
+                registeredCorrelationId[0] = correlationId;
 
                 if (correlationProperty != null) {
                     replyManager.setCorrelationProperty(correlationProperty);
@@ -256,7 +259,13 @@ public class JmsProducer extends DefaultAsyncProducer {
             }
         };
 
-        doSend(exchange, true, destinationName, destination, messageCreator, messageSentCallback);
+        try {
+            doSend(exchange, true, destinationName, destination, messageCreator, messageSentCallback);
+        } catch (Exception e) {
+            // send failed after reply was registered, cancel to prevent double callback on timeout
+            replyManager.cancelCorrelationId(registeredCorrelationId[0]);
+            throw e;
+        }
 
         // continue routing asynchronously (reply will be processed async when its received)
         return false;
diff --git a/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/ReplyManager.java b/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/ReplyManager.java
index a18e20b69ff8..c95de55f8ede 100644
--- a/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/ReplyManager.java
+++ b/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/ReplyManager.java
@@ -101,6 +101,16 @@ public interface ReplyManager extends SessionAwareMessageListener {
      */
     void updateCorrelationId(String correlationId, String newCorrelationId, long requestTimeout);
 
+    /**
+     * Cancels a pending reply correlation, removing it from the correlation map without invoking the callback.
+     * <p/>
+     * This is used when the JMS send fails after the reply has been registered, to prevent the timeout handler from
+     * firing a second callback on an already-completed exchange.
+     *
+     * @param correlationId the correlation id to cancel
+     */
+    void cancelCorrelationId(String correlationId);
+
     /**
      * Process the reply
      *
diff --git a/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/ReplyManagerSupport.java b/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/ReplyManagerSupport.java
index 54e73ddb2601..6f873e8dd3e4 100644
--- a/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/ReplyManagerSupport.java
+++ b/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/ReplyManagerSupport.java
@@ -135,6 +135,16 @@ public abstract class ReplyManagerSupport extends ServiceSupport implements Repl
         return correlationId;
     }
 
+    @Override
+    public void cancelCorrelationId(String correlationId) {
+        if (correlationId != null && correlation != null) {
+            ReplyHandler handler = correlation.remove(correlationId);
+            if (handler != null) {
+                log.debug("Cancelled reply correlation [{}]", correlationId);
+            }
+        }
+    }
+
     protected abstract ReplyHandler createReplyHandler(
             ReplyManager replyManager, Exchange exchange, AsyncCallback callback,
             String originalCorrelationId, String correlationId, long requestTimeout);
diff --git a/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsInOutSendFailureCallbackTest.java b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsInOutSendFailureCallbackTest.java
new file mode 100644
index 000000000000..e6ac4083d6f3
--- /dev/null
+++ b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsInOutSendFailureCallbackTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.jms;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.jms.Connection;
+import jakarta.jms.ConnectionFactory;
+import jakarta.jms.JMSException;
+import jakarta.jms.MessageProducer;
+import jakarta.jms.Session;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.ExchangeTimedOutException;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.infra.core.CamelContextExtension;
+import org.apache.camel.test.infra.core.TransientCamelContextExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.apache.camel.component.jms.JmsComponent.jmsComponentAutoAcknowledge;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Tests that when a JMS send fails after the reply correlation has been registered, the AsyncCallback is invoked
+ * exactly once (not a second time by the timeout handler).
+ *
+ * @see <a href="https://issues.apache.org/jira/browse/CAMEL-24073">CAMEL-24073</a>
+ */
+public class JmsInOutSendFailureCallbackTest extends AbstractJMSTest {
+
+    @Order(2)
+    @RegisterExtension
+    public static CamelContextExtension camelContextExtension = new TransientCamelContextExtension();
+
+    protected CamelContext context;
+    protected ProducerTemplate template;
+
+    @Test
+    public void testCallbackInvokedOnceOnSendFailure() throws Exception {
+        Exchange result = template.send("direct:JmsInOutSendFailureCallbackTest", ExchangePattern.InOut,
+                p -> p.getIn().setBody("Hello"));
+
+        assertNotNull(result.getException());
+        assertFalse(result.getException() instanceof ExchangeTimedOutException,
+                "Should fail with JMS send exception, not ExchangeTimedOutException");
+
+        // wait past the requestTimeout (2s) and verify the timeout handler does not
+        // overwrite the exception with ExchangeTimedOutException via a second callback
+        Exception originalException = result.getException();
+        await().during(3, TimeUnit.SECONDS)
+                .atMost(4, TimeUnit.SECONDS)
+                .untilAsserted(() -> assertSame(originalException, result.getException(),
+                        "Exception changed after send failure - timeout handler fired a second callback"));
+    }
+
+    @Override
+    public String getComponentName() {
+        return "activemq";
+    }
+
+    @Override
+    protected JmsComponent setupComponent(
+            CamelContext camelContext, ConnectionFactory connectionFactory, String componentName) {
+        ConnectionFactory failingCf = createFailingSendConnectionFactory(connectionFactory);
+        return jmsComponentAutoAcknowledge(failingCf);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:JmsInOutSendFailureCallbackTest")
+                        .to(ExchangePattern.InOut,
+                                "activemq:queue:JmsInOutSendFailureCallbackTest?requestTimeout=2000");
+            }
+        };
+    }
+
+    @Override
+    public CamelContextExtension getCamelContextExtension() {
+        return camelContextExtension;
+    }
+
+    @BeforeEach
+    void setUpRequirements() {
+        context = camelContextExtension.getContext();
+        template = camelContextExtension.getProducerTemplate();
+    }
+
+    private static ConnectionFactory createFailingSendConnectionFactory(ConnectionFactory delegate) {
+        return proxyOf(ConnectionFactory.class, delegate, (proxy, method, args) -> {
+            Object result = method.invoke(delegate, args);
+            return result instanceof Connection conn ? wrapConnection(conn) : result;
+        });
+    }
+
+    private static Connection wrapConnection(Connection delegate) {
+        return proxyOf(Connection.class, delegate, (proxy, method, args) -> {
+            Object result = method.invoke(delegate, args);
+            return result instanceof Session session ? wrapSession(session) : result;
+        });
+    }
+
+    private static Session wrapSession(Session delegate) {
+        return proxyOf(Session.class, delegate, (proxy, method, args) -> {
+            Object result = method.invoke(delegate, args);
+            return result instanceof MessageProducer producer ? wrapProducer(producer) : result;
+        });
+    }
+
+    private static MessageProducer wrapProducer(MessageProducer delegate) {
+        return proxyOf(MessageProducer.class, delegate, (proxy, method, args) -> {
+            if ("send".equals(method.getName())) {
+                throw new JMSException("Simulated send failure: broker rejected message");
+            }
+            return method.invoke(delegate, args);
+        });
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T proxyOf(Class<T> iface, T delegate, InvocationHandler handler) {
+        return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class<?>[] { iface }, handler);
+    }
+}