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

apupier 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 06ce5afd122a CAMEL-24946: camel-support - 
KeyValueAggregationRepository should store the completed exchange for recovery
06ce5afd122a is described below

commit 06ce5afd122ad92ac94c28823a2dd8b79f78088e
Author: smjain <[email protected]>
AuthorDate: Wed Sep 23 18:30:53 2026 +0530

    CAMEL-24946: camel-support - KeyValueAggregationRepository should store the 
completed exchange for recovery
    
    KeyValueAggregationRepository.remove deleted the entry of the correlation 
key
    and moved the deleted value to the recovery store, ignoring the exchange 
passed
    to remove. When a group is completed by an incoming exchange 
(completionSize,
    completionPredicate, or the strategy), AggregateProcessor aggregates that
    exchange in memory and does not add the result to the repository before it
    removes the group, so the stored value was the group without the last
    exchange(s). If routing the aggregated exchange failed, the recover task
    re-delivered the group without its last message, and without the
    AGGREGATED_CORRELATION_KEY property.
    
    Store the exchange passed to remove in the recovery store instead, like
    JdbcAggregationRepository does. The recovery copy is still only written when
    the key was actually removed.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../AggregateKeyValueRepositoryRecoverTest.java    | 66 ++++++++++++++++++++++
 .../support/KeyValueAggregationRepositoryTest.java | 17 ++++++
 .../support/KeyValueAggregationRepository.java     |  7 ++-
 3 files changed, 88 insertions(+), 2 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregateKeyValueRepositoryRecoverTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregateKeyValueRepositoryRecoverTest.java
new file mode 100644
index 000000000000..ca92ec5b0897
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregateKeyValueRepositoryRecoverTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.processor.BodyInAggregatingStrategy;
+import org.apache.camel.support.KeyValueAggregationRepository;
+import org.junit.jupiter.api.Test;
+
+/**
+ * An aggregated exchange recovered from a {@link 
KeyValueAggregationRepository} must be the exchange that was
+ * completed, including the exchange that completed the group.
+ */
+public class AggregateKeyValueRepositoryRecoverTest extends ContextTestSupport 
{
+
+    @Test
+    public void testRecoverGroupCompletedBySize() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedBodiesReceived("A+B+C", "A+B+C");
+        mock.message(0).header(Exchange.REDELIVERED).isNull();
+        mock.message(1).header(Exchange.REDELIVERED).isEqualTo(true);
+        // the first attempt fails, so the aggregated exchange is recovered
+        mock.whenExchangeReceived(1, exchange -> {
+            throw new IllegalArgumentException("Forced");
+        });
+
+        template.sendBodyAndHeader("direct:start", "A", "id", 1);
+        template.sendBodyAndHeader("direct:start", "B", "id", 1);
+        template.sendBodyAndHeader("direct:start", "C", "id", 1);
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                KeyValueAggregationRepository repository = new 
KeyValueAggregationRepository();
+                repository.setRecoveryInterval(100);
+
+                from("direct:start")
+                        .aggregate(header("id"), new 
BodyInAggregatingStrategy()).aggregationRepository(repository)
+                        .completionSize(3)
+                        .to("mock:result");
+            }
+        };
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java
index 4debfc8d8ad7..d40b85f84bc7 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java
@@ -141,6 +141,23 @@ class KeyValueAggregationRepositoryTest {
         assertThat(aggregationRepository.recover(camelContext, 
exchangeId)).isNull();
     }
 
+    @Test
+    void testRemoveStoresGivenExchangeForRecovery() {
+        aggregationRepository.add(camelContext, "key1", createExchange("A"));
+
+        // the group is completed by an exchange that is aggregated but not 
added to the repository
+        Exchange aggregated = aggregationRepository.get(camelContext, "key1");
+        aggregated.getIn().setBody("A+B");
+        aggregated.setProperty(Exchange.AGGREGATED_CORRELATION_KEY, "key1");
+        aggregationRepository.remove(camelContext, "key1", aggregated);
+
+        Exchange recovered = aggregationRepository.recover(camelContext, 
aggregated.getExchangeId());
+        assertThat(recovered).isNotNull();
+        
assertThat(recovered.getExchangeId()).isEqualTo(aggregated.getExchangeId());
+        assertThat(recovered.getIn().getBody(String.class)).isEqualTo("A+B");
+        assertThat(recovered.getProperty(Exchange.AGGREGATED_CORRELATION_KEY, 
String.class)).isEqualTo("key1");
+    }
+
     @Test
     void testScanReturnsCompletedExchangeIds() {
         Exchange ex1 = createExchange("One");
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java
index 38a4aaf24301..ccf651a8c266 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java
@@ -124,9 +124,12 @@ public class KeyValueAggregationRepository extends 
ServiceSupport
         LOG.trace("Removing an Exchange with ID {} for key {}", 
exchange.getExchangeId(), key);
         DefaultExchangeHolder holder = (DefaultExchangeHolder) 
repository.delete(AGGREGATE_PREFIX + key);
         if (useRecovery && holder != null) {
-            // Store under the exchangeId for potential recovery
+            // Store the given exchange under the exchangeId for potential 
recovery. The removed holder is not used as
+            // it can be older than the exchange: when a group is completed by 
an incoming exchange, the last
+            // aggregated exchange is not added to the repository before it is 
removed.
             LOG.trace("Moving Exchange with ID {} to completed (pending 
confirmation)", exchange.getExchangeId());
-            repository.put(COMPLETED_PREFIX + exchange.getExchangeId(), 
holder);
+            repository.put(COMPLETED_PREFIX + exchange.getExchangeId(),
+                    DefaultExchangeHolder.marshal(exchange, true, 
allowSerializedHeaders));
         }
     }
 

Reply via email to