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 e1c7f27d0c8c CAMEL-24124: Drain pending reply handlers during shutdown 
instead of silently discarding
e1c7f27d0c8c is described below

commit e1c7f27d0c8c4df8692cb0a871c16b3c15626bc8
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Jul 16 07:13:48 2026 +0200

    CAMEL-24124: Drain pending reply handlers during shutdown instead of 
silently discarding
    
    Fix DefaultTimeoutMap to emit Evict events for remaining entries during
    stop instead of silently calling map.clear(). This ensures reply-manager
    listeners are notified. The drain-and-evict logic is exposed as a
    protected method so only reply-manager subclasses (camel-jms, camel-sjms,
    camel-rocketmq) opt in — preserving the aggregator's restart semantics.
    
    Relax the isRunAllowed() guard in processReply() to allow reply
    processing during the STOPPING phase. Replies arriving during shutdown
    are processed normally; pending entries that never received a reply are
    drained with RejectedExecutionException and their callbacks completed.
    
    This fixes a 45-second graceful-shutdown stall caused by InOut exchanges
    whose AsyncCallback was never completed when the reply manager stopped.
    
    Closes #24742
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../component/jms/reply/CorrelationTimeoutMap.java |  6 ++
 .../component/jms/reply/ReplyManagerSupport.java   | 10 ++-
 .../component/rocketmq/reply/ReplyTimeoutMap.java  |  6 ++
 .../reply/RocketMQReplyManagerSupport.java         | 26 ++++--
 .../sjms/reply/CorrelationTimeoutMap.java          |  6 ++
 .../component/sjms/reply/ReplyManagerSupport.java  | 94 +++++++++++-----------
 .../apache/camel/support/DefaultTimeoutMap.java    | 22 +++++
 7 files changed, 114 insertions(+), 56 deletions(-)

diff --git 
a/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/CorrelationTimeoutMap.java
 
b/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/CorrelationTimeoutMap.java
index 2908918c009c..efd5316f6ba2 100644
--- 
a/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/CorrelationTimeoutMap.java
+++ 
b/components/camel-jms/src/main/java/org/apache/camel/component/jms/reply/CorrelationTimeoutMap.java
@@ -57,6 +57,12 @@ class CorrelationTimeoutMap extends 
DefaultTimeoutMap<String, ReplyHandler> {
         }
     }
 
+    @Override
+    protected void doStop() throws Exception {
+        drainAndEvictAll();
+        super.doStop();
+    }
+
     @Override
     public ReplyHandler put(String key, ReplyHandler value, long 
timeoutMillis) {
         return super.put(key, value, encode(timeoutMillis));
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 6f873e8dd3e4..870ad77d7c90 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
@@ -19,6 +19,7 @@ package org.apache.camel.component.jms.reply;
 import java.time.Duration;
 import java.util.Objects;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 
 import jakarta.jms.Destination;
@@ -176,13 +177,18 @@ public abstract class ReplyManagerSupport extends 
ServiceSupport implements Repl
 
     @Override
     public void processReply(ReplyHolder holder) {
-        if (holder == null || !isRunAllowed()) {
+        if (holder == null) {
             return;
         }
         try {
             Exchange exchange = holder.getExchange();
             if (holder.isTimeout()) {
-                handleTimeout(holder, exchange);
+                if (isStopping() || isStopped()) {
+                    exchange.setException(new RejectedExecutionException(
+                            "Cannot process JMS reply as the reply manager is 
shutting down"));
+                } else {
+                    handleTimeout(holder, exchange);
+                }
             } else {
                 handleSuccessfulReply(holder, exchange);
             }
diff --git 
a/components/camel-rocketmq/src/main/java/org/apache/camel/component/rocketmq/reply/ReplyTimeoutMap.java
 
b/components/camel-rocketmq/src/main/java/org/apache/camel/component/rocketmq/reply/ReplyTimeoutMap.java
index b60d43038588..c567ef363b1b 100644
--- 
a/components/camel-rocketmq/src/main/java/org/apache/camel/component/rocketmq/reply/ReplyTimeoutMap.java
+++ 
b/components/camel-rocketmq/src/main/java/org/apache/camel/component/rocketmq/reply/ReplyTimeoutMap.java
@@ -53,6 +53,12 @@ public class ReplyTimeoutMap extends 
DefaultTimeoutMap<String, ReplyHandler> {
         }
     }
 
+    @Override
+    protected void doStop() throws Exception {
+        drainAndEvictAll();
+        super.doStop();
+    }
+
     @Override
     public ReplyHandler put(String key, ReplyHandler value, long 
timeoutMillis) {
         return super.put(key, value, encode(timeoutMillis));
diff --git 
a/components/camel-rocketmq/src/main/java/org/apache/camel/component/rocketmq/reply/RocketMQReplyManagerSupport.java
 
b/components/camel-rocketmq/src/main/java/org/apache/camel/component/rocketmq/reply/RocketMQReplyManagerSupport.java
index ec2b025d5d82..ed217441c71e 100644
--- 
a/components/camel-rocketmq/src/main/java/org/apache/camel/component/rocketmq/reply/RocketMQReplyManagerSupport.java
+++ 
b/components/camel-rocketmq/src/main/java/org/apache/camel/component/rocketmq/reply/RocketMQReplyManagerSupport.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.rocketmq.reply;
 
 import java.util.Arrays;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 
 import org.apache.camel.AsyncCallback;
@@ -148,20 +149,18 @@ public class RocketMQReplyManagerSupport extends 
ServiceSupport implements Reply
 
     @Override
     public void processReply(ReplyHolder holder) {
-        if (!isRunAllowed()) {
+        if (holder == null) {
             return;
         }
         try {
             Exchange exchange = holder.getExchange();
             if (holder.isTimeout()) {
-                if (log.isWarnEnabled()) {
-                    log.warn("Timeout occurred after {} millis waiting for 
reply message with messageKey [{}] on topic {}." +
-                             " Setting ExchangeTimedOutException on {} and 
continue routing.",
-                            holder.getTimeout(), holder.getMessageKey(), 
replyToTopic, ExchangeHelper.logIds(exchange));
+                if (isStopping() || isStopped()) {
+                    exchange.setException(new RejectedExecutionException(
+                            "Cannot process reply as the reply manager is 
shutting down"));
+                } else {
+                    handleTimeout(holder, exchange);
                 }
-                String msg = "reply message with messageKey: " + 
holder.getMessageKey() + " not received on topic: "
-                             + replyToTopic;
-                exchange.setException(new ExchangeTimedOutException(exchange, 
holder.getTimeout(), msg));
             } else {
                 processReceivedReply(holder);
             }
@@ -170,6 +169,17 @@ public class RocketMQReplyManagerSupport extends 
ServiceSupport implements Reply
         }
     }
 
+    private void handleTimeout(ReplyHolder holder, Exchange exchange) {
+        if (log.isWarnEnabled()) {
+            log.warn("Timeout occurred after {} millis waiting for reply 
message with messageKey [{}] on topic {}." +
+                     " Setting ExchangeTimedOutException on {} and continue 
routing.",
+                    holder.getTimeout(), holder.getMessageKey(), replyToTopic, 
ExchangeHelper.logIds(exchange));
+        }
+        String msg = "reply message with messageKey: " + 
holder.getMessageKey() + " not received on topic: "
+                     + replyToTopic;
+        exchange.setException(new ExchangeTimedOutException(exchange, 
holder.getTimeout(), msg));
+    }
+
     private static void processReceivedReply(ReplyHolder holder) {
         Message message = holder.getExchange().getOut();
         MessageExt messageExt = holder.getMessageExt();
diff --git 
a/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/CorrelationTimeoutMap.java
 
b/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/CorrelationTimeoutMap.java
index 584c2c3bcc65..632a0413b67c 100644
--- 
a/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/CorrelationTimeoutMap.java
+++ 
b/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/CorrelationTimeoutMap.java
@@ -57,6 +57,12 @@ class CorrelationTimeoutMap extends 
DefaultTimeoutMap<String, ReplyHandler> {
         }
     }
 
+    @Override
+    protected void doStop() throws Exception {
+        drainAndEvictAll();
+        super.doStop();
+    }
+
     @Override
     public ReplyHandler put(String key, ReplyHandler value, long 
timeoutMillis) {
         return super.put(key, value, encode(timeoutMillis));
diff --git 
a/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/ReplyManagerSupport.java
 
b/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/ReplyManagerSupport.java
index 3c2603cfb435..e2800ee1fcde 100644
--- 
a/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/ReplyManagerSupport.java
+++ 
b/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/reply/ReplyManagerSupport.java
@@ -18,6 +18,7 @@ package org.apache.camel.component.sjms.reply;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
@@ -143,55 +144,56 @@ public abstract class ReplyManagerSupport extends 
ServiceSupport implements Repl
 
     @Override
     public void processReply(ReplyHolder holder) {
-        if (holder != null && isRunAllowed()) {
-            try {
-                Exchange exchange = holder.getExchange();
-
-                boolean timeout = holder.isTimeout();
-                if (timeout) {
-                    // timeout occurred do a WARN log so its easier to spot in 
the logs
-                    if (log.isWarnEnabled()) {
-                        log.warn(
-                                "Timeout occurred after {} millis waiting for 
reply message with correlationID [{}] on destination {}."
-                                 + " Setting ExchangeTimedOutException on {} 
and continue routing.",
-                                holder.getRequestTimeout(), 
holder.getCorrelationId(), replyTo,
-                                ExchangeHelper.logIds(exchange));
-                    }
-
-                    // no response, so lets set a timed out exception
-                    String msg = "reply message with correlationID: " + 
holder.getCorrelationId()
-                                 + " not received on destination: " + replyTo;
-                    exchange.setException(new 
ExchangeTimedOutException(exchange, holder.getRequestTimeout(), msg));
+        if (holder == null) {
+            return;
+        }
+        try {
+            Exchange exchange = holder.getExchange();
+            if (holder.isTimeout()) {
+                if (isStopping() || isStopped()) {
+                    exchange.setException(new RejectedExecutionException(
+                            "Cannot process JMS reply as the reply manager is 
shutting down"));
                 } else {
-                    Message message = holder.getMessage();
-                    Session session = holder.getSession();
-                    SjmsMessage response = new SjmsMessage(exchange, message, 
session, endpoint.getBinding());
-                    // the JmsBinding is designed to be "pull-based": it will 
populate the Camel message on demand
-                    // therefore, we link Exchange and OUT message before 
continuing, so that the JmsBinding has full access
-                    // to everything it may need, and can populate headers, 
properties, etc. accordingly (solves CAMEL-6218).
-                    exchange.setOut(response);
-                    Object body = response.getBody();
-
-                    if (endpoint.isTransferException() && body instanceof 
Exception exception) {
-                        log.debug("Reply was an Exception. Setting the 
Exception on the Exchange: {}", body);
-                        // we got an exception back and endpoint was 
configured to transfer exception
-                        // therefore set response as exception
-                        exchange.setException(exception);
-                    } else {
-                        log.debug("Reply received. OUT message body set to 
reply payload: {}", body);
-                    }
-
-                    // restore correlation id in case the remote server messed 
with it
-                    if (holder.getOriginalCorrelationId() != null) {
-                        JmsMessageHelper.setCorrelationId(message, 
holder.getOriginalCorrelationId());
-                        
exchange.getOut().setHeader(SjmsConstants.JMS_CORRELATION_ID, 
holder.getOriginalCorrelationId());
-                    }
+                    handleTimeout(holder, exchange);
                 }
-            } finally {
-                // notify callback
-                AsyncCallback callback = holder.getCallback();
-                callback.done(false);
+            } else {
+                handleSuccessfulReply(holder, exchange);
             }
+        } finally {
+            holder.getCallback().done(false);
+        }
+    }
+
+    private void handleTimeout(ReplyHolder holder, Exchange exchange) {
+        if (log.isWarnEnabled()) {
+            log.warn(
+                    "Timeout occurred after {} millis waiting for reply 
message with correlationID [{}] on destination {}."
+                     + " Setting ExchangeTimedOutException on {} and continue 
routing.",
+                    holder.getRequestTimeout(), holder.getCorrelationId(), 
replyTo,
+                    ExchangeHelper.logIds(exchange));
+        }
+        String msg = "reply message with correlationID: " + 
holder.getCorrelationId()
+                     + " not received on destination: " + replyTo;
+        exchange.setException(new ExchangeTimedOutException(exchange, 
holder.getRequestTimeout(), msg));
+    }
+
+    private void handleSuccessfulReply(ReplyHolder holder, Exchange exchange) {
+        Message message = holder.getMessage();
+        Session session = holder.getSession();
+        SjmsMessage response = new SjmsMessage(exchange, message, session, 
endpoint.getBinding());
+        exchange.setOut(response);
+        Object body = response.getBody();
+
+        if (endpoint.isTransferException() && body instanceof Exception 
exception) {
+            log.debug("Reply was an Exception. Setting the Exception on the 
Exchange: {}", body);
+            exchange.setException(exception);
+        } else {
+            log.debug("Reply received. OUT message body set to reply payload: 
{}", body);
+        }
+
+        if (holder.getOriginalCorrelationId() != null) {
+            JmsMessageHelper.setCorrelationId(message, 
holder.getOriginalCorrelationId());
+            exchange.getOut().setHeader(SjmsConstants.JMS_CORRELATION_ID, 
holder.getOriginalCorrelationId());
         }
     }
 
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultTimeoutMap.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultTimeoutMap.java
index b6cda4f3eccd..e64f980078df 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultTimeoutMap.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultTimeoutMap.java
@@ -279,6 +279,28 @@ public class DefaultTimeoutMap<K, V> extends 
ServiceSupport implements TimeoutMa
         schedulePoll();
     }
 
+    /**
+     * Drain all remaining entries from the map, emitting an {@link 
Listener.Type#Evict} event for each one. Subclasses
+     * that need to notify listeners about leftover entries on shutdown (e.g. 
reply-manager timeout maps) should call
+     * this from their {@link #doStop()} override before delegating to {@code 
super.doStop()}.
+     */
+    protected void drainAndEvictAll() {
+        if (map.isEmpty()) {
+            return;
+        }
+        List<TimeoutMapEntry<K, V>> remaining;
+        lock.lock();
+        try {
+            remaining = new ArrayList<>(map.values());
+            map.clear();
+        } finally {
+            lock.unlock();
+        }
+        for (TimeoutMapEntry<K, V> entry : remaining) {
+            emitEvent(Evict, entry.getKey(), entry.getValue());
+        }
+    }
+
     @Override
     protected void doStop() throws Exception {
         if (future != null) {

Reply via email to