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 93950e50adf0 CAMEL-24141: Aggregate EIP - guard background tasks
against exceptions
93950e50adf0 is described below
commit 93950e50adf0c62b7d24f012bac70e2f0a9f39dc
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jul 17 16:00:59 2026 +0200
CAMEL-24141: Aggregate EIP - guard background tasks against exceptions
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../processor/aggregate/AggregateProcessor.java | 67 ++++++------
.../AggregateIntervalTaskExceptionTest.java | 112 +++++++++++++++++++++
2 files changed, 151 insertions(+), 28 deletions(-)
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 96cadd4ba3b5..a9c8debbb09c 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
@@ -923,6 +923,9 @@ public class AggregateProcessor extends BaseProcessorSupport
private void aggregateCompletionCounter(Exchange exchange) {
String completedBy =
exchange.getProperty(ExchangePropertyKey.AGGREGATED_COMPLETED_BY, String.class);
+ if (completedBy == null) {
+ return;
+ }
switch (completedBy) {
case COMPLETED_BY_INTERVAL:
completedByInterval.incrementAndGet();
@@ -1396,40 +1399,45 @@ public class AggregateProcessor extends
BaseProcessorSupport
LOG.trace("Starting completion interval task");
- // trigger completion for all in the repository
- Set<String> keys = aggregationRepository.getKeys();
+ // must catch and log exception otherwise the executor will not
schedule next interval task
+ try {
+ // trigger completion for all in the repository
+ Set<String> keys = aggregationRepository.getKeys();
- if (keys != null && !keys.isEmpty()) {
- // must acquire the shared aggregation lock to be able to
trigger interval completion
- lock.lock();
- try {
- for (String key : keys) {
- boolean stolenInterval = false;
- Exchange exchange =
aggregationRepository.get(camelContext, key);
- if (exchange == null) {
- stolenInterval = true;
- } else {
- LOG.trace("Completion interval triggered for
correlation key: {}", key);
- // indicate it was completed by interval
-
exchange.setProperty(ExchangePropertyKey.AGGREGATED_COMPLETED_BY,
COMPLETED_BY_INTERVAL);
- try {
- Exchange answer = onCompletion(key, exchange,
exchange, false, false);
- if (answer != null) {
- onSubmitCompletion(key, answer);
- }
- } catch
(OptimisticLockingAggregationRepository.OptimisticLockingException e) {
+ if (keys != null && !keys.isEmpty()) {
+ // must acquire the shared aggregation lock to be able to
trigger interval completion
+ lock.lock();
+ try {
+ for (String key : keys) {
+ boolean stolenInterval = false;
+ Exchange exchange =
aggregationRepository.get(camelContext, key);
+ if (exchange == null) {
stolenInterval = true;
+ } else {
+ LOG.trace("Completion interval triggered for
correlation key: {}", key);
+ // indicate it was completed by interval
+
exchange.setProperty(ExchangePropertyKey.AGGREGATED_COMPLETED_BY,
COMPLETED_BY_INTERVAL);
+ try {
+ Exchange answer = onCompletion(key,
exchange, exchange, false, false);
+ if (answer != null) {
+ onSubmitCompletion(key, answer);
+ }
+ } catch
(OptimisticLockingAggregationRepository.OptimisticLockingException e) {
+ stolenInterval = true;
+ }
+ }
+ if (optimisticLocking && stolenInterval) {
+ LOG.debug(
+ "Another Camel instance has already
processed this interval aggregation for exchange with correlation id: {}",
+ key);
}
}
- if (optimisticLocking && stolenInterval) {
- LOG.debug(
- "Another Camel instance has already
processed this interval aggregation for exchange with correlation id: {}",
- key);
- }
+ } finally {
+ lock.unlock();
}
- } finally {
- lock.unlock();
}
+ } catch (Exception e) {
+ LOG.warn("Error during completion interval task. This
exception is ignored.", e);
}
LOG.trace("Completion interval task complete");
@@ -1549,6 +1557,9 @@ public class AggregateProcessor extends
BaseProcessorSupport
lock.unlock();
}
}
+ } catch (Exception e) {
+ // must catch and log exception otherwise the executor will
not schedule next recover task
+ LOG.warn("Error during recover task. This exception is
ignored.", e);
} finally {
recoveryInProgress.set(false);
inProgressCompleteExchangesForRecoveryTask.clear();
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregateIntervalTaskExceptionTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregateIntervalTaskExceptionTest.java
new file mode 100644
index 000000000000..67d3abac8b43
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregateIntervalTaskExceptionTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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 java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+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.aggregate.MemoryAggregationRepository;
+import org.apache.camel.processor.aggregate.UseLatestAggregationStrategy;
+import org.apache.camel.spi.AggregationRepository;
+import org.apache.camel.support.service.ServiceSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test that the completion interval task survives an exception thrown by the
aggregation repository. Without the
+ * try/catch guard in AggregationIntervalTask.run(), a single RuntimeException
from getKeys() would permanently cancel
+ * the scheduled task.
+ */
+public class AggregateIntervalTaskExceptionTest extends ContextTestSupport {
+
+ @Test
+ public void testIntervalTaskSurvivesException() throws Exception {
+ getMockEndpoint("mock:result").expectedMinimumMessageCount(1);
+
+ // send messages that will be aggregated
+ template.sendBodyAndHeader("direct:start", "A", "id", "1");
+ template.sendBodyAndHeader("direct:start", "B", "id", "1");
+
+ // the first interval tick with data will hit the exception from
getKeys(),
+ // but the scheduler should survive and complete on a subsequent tick
+ MockEndpoint.assertIsSatisfied(context, 15, TimeUnit.SECONDS);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start")
+ .aggregate(header("id"), new
UseLatestAggregationStrategy())
+ .aggregationRepository(new FailOnceRepository())
+ .completionInterval(500)
+ .to("mock:result");
+ }
+ };
+ }
+
+ private static class FailOnceRepository extends ServiceSupport implements
AggregationRepository {
+
+ private final MemoryAggregationRepository delegate = new
MemoryAggregationRepository();
+ private final AtomicInteger getKeysCount = new AtomicInteger();
+
+ @Override
+ public Exchange add(CamelContext camelContext, String key, Exchange
exchange) {
+ return delegate.add(camelContext, key, exchange);
+ }
+
+ @Override
+ public Exchange get(CamelContext camelContext, String key) {
+ return delegate.get(camelContext, key);
+ }
+
+ @Override
+ public void remove(CamelContext camelContext, String key, Exchange
exchange) {
+ delegate.remove(camelContext, key, exchange);
+ }
+
+ @Override
+ public void confirm(CamelContext camelContext, String exchangeId) {
+ delegate.confirm(camelContext, exchangeId);
+ }
+
+ @Override
+ public Set<String> getKeys() {
+ Set<String> keys = delegate.getKeys();
+ if (keys != null && !keys.isEmpty() &&
getKeysCount.incrementAndGet() == 1) {
+ throw new RuntimeException("Simulated repository failure");
+ }
+ return keys;
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ delegate.start();
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ delegate.stop();
+ }
+ }
+}