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 abb77d3e4aeb CAMEL-26641: camel-core - Aggregate EIP: a suspend should 
not force the completion of the groups (#29371)
abb77d3e4aeb is described below

commit abb77d3e4aebc21b04064e555e53981ed9c9327d
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Nov 7 13:31:32 2026 +0530

    CAMEL-26641: camel-core - Aggregate EIP: a suspend should not force the 
completion of the groups (#29371)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/catalog/docs/aggregate-eip.adoc   |   4 +-
 .../docs/modules/eips/pages/aggregate-eip.adoc     |   4 +-
 .../processor/aggregate/AggregateProcessor.java    |  10 ++
 .../aggregator/AggregateSuspendResumeTest.java     | 142 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   6 +
 5 files changed, 164 insertions(+), 2 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aggregate-eip.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aggregate-eip.adoc
index 9a968b7654f7..ea076943b21b 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aggregate-eip.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aggregate-eip.adoc
@@ -267,7 +267,9 @@ for more details.
 xref:manual::batch-consumer.adoc[Batch Consumer], which allows you to complete
 when all the messages from the batch have been aggregated.
 * _forceCompletionOnStop_: Indicates to complete all current
-aggregated exchanges when the context is stopped
+aggregated exchanges when the context (or the route) is stopped. A suspend
+does not force their completion: the groups go on aggregating after the resume
+(completionTimeout and completionInterval still apply).
 * _AggregateController_: which allows to use an external source 
(`AggregateController` implementation) to complete groups or all groups.
 This can be done using Java or JMX API.
 
diff --git 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/aggregate-eip.adoc 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/aggregate-eip.adoc
index 9a968b7654f7..ea076943b21b 100644
--- a/core/camel-core-engine/src/main/docs/modules/eips/pages/aggregate-eip.adoc
+++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/aggregate-eip.adoc
@@ -267,7 +267,9 @@ for more details.
 xref:manual::batch-consumer.adoc[Batch Consumer], which allows you to complete
 when all the messages from the batch have been aggregated.
 * _forceCompletionOnStop_: Indicates to complete all current
-aggregated exchanges when the context is stopped
+aggregated exchanges when the context (or the route) is stopped. A suspend
+does not force their completion: the groups go on aggregating after the resume
+(completionTimeout and completionInterval still apply).
 * _AggregateController_: which allows to use an external source 
(`AggregateController` implementation) to complete groups or all groups.
 This can be done using Java or JMX API.
 
diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregateProcessor.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregateProcessor.java
index 087991e9681f..e2319d134352 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregateProcessor.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregateProcessor.java
@@ -2086,6 +2086,10 @@ public class AggregateProcessor extends 
BaseProcessorSupport
 
     @Override
     public void prepareShutdown(boolean suspendOnly, boolean forced) {
+        if (suspendOnly) {
+            // only complete the groups when stopping (not when suspending)
+            return;
+        }
         // we are shutting down, so force completion if this option was enabled
         // but only do this when forced=false, as that is when we have chance 
to
         // send out new messages to be routed by Camel. When forced=true, then
@@ -2101,6 +2105,12 @@ public class AggregateProcessor extends 
BaseProcessorSupport
         return true;
     }
 
+    @Override
+    public int getPendingExchangesSize(boolean suspendOnly) {
+        // a suspend keeps the groups (they are not completed), so there is 
nothing to wait for
+        return suspendOnly ? 0 : getPendingExchangesSize();
+    }
+
     @Override
     public int getPendingExchangesSize() {
         if (completeAllOnStop) {
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregateSuspendResumeTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregateSuspendResumeTest.java
new file mode 100644
index 000000000000..17ecd3621be0
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregateSuspendResumeTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.processor.aggregator;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.ServiceStatus;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.processor.BodyInAggregatingStrategy;
+import org.apache.camel.util.StopWatch;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * forceCompletionOnStop and completeAllOnStop complete the groups when the 
route or the CamelContext is stopped. A
+ * suspend is not a stop: the groups are kept and go on aggregating after the 
resume, and a later stop completes them.
+ */
+class AggregateSuspendResumeTest extends ContextTestSupport {
+
+    @Test
+    void testForceCompletionOnStopKeepsGroupsOnRouteSuspend() throws Exception 
{
+        sendTwo("direct:force");
+
+        context.getRouteController().suspendRoute("force");
+        assertEquals(ServiceStatus.Suspended, 
context.getRouteController().getRouteStatus("force"));
+        assertEquals(0, getMockEndpoint("mock:force").getReceivedCounter(), "a 
suspend completed the group");
+
+        context.getRouteController().resumeRoute("force");
+        expectGroupCompletedAfterResume("direct:force", "mock:force");
+    }
+
+    @Test
+    void testCompleteAllOnStopKeepsGroupsOnRouteSuspend() throws Exception {
+        sendTwo("direct:all");
+
+        // the shutdown timeout of the tests is 10 seconds
+        StopWatch watch = new StopWatch();
+        context.getRouteController().suspendRoute("all");
+        assertTrue(watch.taken() < 5000, "the suspend waited for the groups to 
complete: " + watch.taken() + " ms");
+        assertEquals(ServiceStatus.Suspended, 
context.getRouteController().getRouteStatus("all"));
+        assertEquals(0, getMockEndpoint("mock:all").getReceivedCounter(), "a 
suspend completed the group");
+
+        context.getRouteController().resumeRoute("all");
+        expectGroupCompletedAfterResume("direct:all", "mock:all");
+    }
+
+    @Test
+    void testForceCompletionOnStopKeepsGroupsOnCamelContextSuspend() throws 
Exception {
+        // control: a CamelContext suspend did not complete the groups before 
either (the context is not started)
+        sendTwo("direct:force");
+
+        context.suspend();
+        assertEquals(0, getMockEndpoint("mock:force").getReceivedCounter(), "a 
suspend completed the group");
+
+        context.resume();
+        expectGroupCompletedAfterResume("direct:force", "mock:force");
+    }
+
+    @Test
+    void testCompleteAllOnStopKeepsGroupsOnCamelContextSuspend() throws 
Exception {
+        sendTwo("direct:all");
+
+        // the shutdown timeout of the tests is 10 seconds
+        StopWatch watch = new StopWatch();
+        context.suspend();
+        assertTrue(watch.taken() < 5000, "the suspend waited for the groups to 
complete: " + watch.taken() + " ms");
+        assertEquals(0, getMockEndpoint("mock:all").getReceivedCounter(), "a 
suspend completed the group");
+
+        context.resume();
+        expectGroupCompletedAfterResume("direct:all", "mock:all");
+    }
+
+    @Test
+    void testForceCompletionOnStopCompletesKeptGroupsOnStopAfterSuspend() 
throws Exception {
+        expectGroupsCompletedOnStopAfterSuspend("direct:force", "force", 
"mock:force");
+    }
+
+    @Test
+    void testCompleteAllOnStopCompletesKeptGroupsOnStopAfterSuspend() throws 
Exception {
+        expectGroupsCompletedOnStopAfterSuspend("direct:all", "all", 
"mock:all");
+    }
+
+    private void expectGroupsCompletedOnStopAfterSuspend(String uri, String 
routeId, String mockUri) throws Exception {
+        MockEndpoint mock = getMockEndpoint(mockUri);
+        mock.expectedBodiesReceived("A+B");
+
+        sendTwo(uri);
+        context.getRouteController().suspendRoute(routeId);
+        assertEquals(0, mock.getReceivedCounter(), "a suspend completed the 
group");
+        context.getRouteController().stopRoute(routeId);
+
+        mock.assertIsSatisfied();
+    }
+
+    private void sendTwo(String uri) {
+        template.sendBodyAndHeader(uri, "A", "id", "foo");
+        template.sendBodyAndHeader(uri, "B", "id", "foo");
+    }
+
+    private void expectGroupCompletedAfterResume(String uri, String mockUri) 
throws Exception {
+        MockEndpoint mock = getMockEndpoint(mockUri);
+        mock.expectedBodiesReceived("A+B+C");
+
+        template.sendBodyAndHeader(uri, "C", "id", "foo");
+
+        mock.assertIsSatisfied();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:force").routeId("force")
+                        .aggregate(header("id"), new 
BodyInAggregatingStrategy())
+                        .completionSize(3).forceCompletionOnStop()
+                        .to("mock:force");
+
+                from("direct:all").routeId("all")
+                        .aggregate(header("id"), new 
BodyInAggregatingStrategy())
+                        .completionSize(3).completeAllOnStop()
+                        .to("mock:all");
+            }
+        };
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index fa61d8a51871..d66a5266da36 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -333,6 +333,12 @@ let such a failure skip the dead letter channel or 
`onException` as before; a pl
 same handling. Better, fix the processor to set the exception on the exchange 
and call the callback, as an
 `AsyncProcessor` must.
 
+=== camel-core - Aggregate EIP no longer completes the groups when the route 
is suspended
+
+With `forceCompletionOnStop` or `completeAllOnStop`, suspending the route no 
longer completes the aggregated groups.
+They are kept and completed when the route or Camel is stopped. To send them 
out before a suspend, use
+`forceCompletionOfAllGroups` (`AggregateController` or JMX), or stop the route 
instead.
+
 === camel-core - ThrottlingExceptionRoutePolicy counts the failures within the 
failure window
 
 The circuit of `ThrottlingExceptionRoutePolicy` now opens when 
`failureThreshold` failures occurred within the last

Reply via email to