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 10065b6f7242 CAMEL-24143: Fix medium findings in Split & Aggregate EIP
processors
10065b6f7242 is described below
commit 10065b6f72424a8f46ab85881021720a2d171358
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jul 18 11:44:07 2026 +0200
CAMEL-24143: Fix medium findings in Split & Aggregate EIP processors
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../main/java/org/apache/camel/SplitResult.java | 16 ++++-
.../java/org/apache/camel/processor/Splitter.java | 73 +++++++++++++++++-----
.../processor/aggregate/AggregateProcessor.java | 12 +++-
.../processor/SplitterErrorThresholdTest.java | 4 +-
.../camel/processor/SplitterSplitResultTest.java | 3 +-
5 files changed, 84 insertions(+), 24 deletions(-)
diff --git a/core/camel-api/src/main/java/org/apache/camel/SplitResult.java
b/core/camel-api/src/main/java/org/apache/camel/SplitResult.java
index 6dbf371ef6ba..a8d125642be8 100644
--- a/core/camel-api/src/main/java/org/apache/camel/SplitResult.java
+++ b/core/camel-api/src/main/java/org/apache/camel/SplitResult.java
@@ -40,12 +40,14 @@ public final class SplitResult {
}
private final int totalItems;
+ private final int processedItems;
private final int failureCount;
private final List<Failure> failures;
private final boolean aborted;
- public SplitResult(int totalItems, int failureCount, List<Failure>
failures, boolean aborted) {
+ public SplitResult(int totalItems, int processedItems, int failureCount,
List<Failure> failures, boolean aborted) {
this.totalItems = totalItems;
+ this.processedItems = processedItems;
this.failureCount = failureCount;
this.failures = failures != null ?
Collections.unmodifiableList(failures) : Collections.emptyList();
this.aborted = aborted;
@@ -59,11 +61,20 @@ public final class SplitResult {
return totalItems;
}
+ /**
+ * The number of items that were actually processed. After an abort, this
may be less than {@code getTotalItems()}.
+ *
+ * @since 4.22
+ */
+ public int getProcessedItems() {
+ return processedItems;
+ }
+
/**
* The number of items that completed successfully.
*/
public int getSuccessCount() {
- return totalItems - failureCount;
+ return processedItems - failureCount;
}
/**
@@ -90,6 +101,7 @@ public final class SplitResult {
@Override
public String toString() {
return "SplitResult[total=" + totalItems
+ + ", processed=" + processedItems
+ ", success=" + getSuccessCount()
+ ", failures=" + failureCount
+ ", aborted=" + aborted + "]";
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
index e6e7705c84c3..78a89bdc33ba 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
@@ -55,6 +55,7 @@ import org.apache.camel.support.resume.OffsetKeys;
import org.apache.camel.support.resume.Offsets;
import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StopWatch;
import org.apache.camel.util.StringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -318,7 +319,15 @@ public class Splitter extends MulticastProcessor {
storedIndex = readCurrentWatermark();
}
if (storedIndex != null) {
- int skipTo = Integer.parseInt(storedIndex);
+ int skipTo;
+ try {
+ skipTo = Integer.parseInt(storedIndex);
+ } catch (NumberFormatException e) {
+ throw new RuntimeCamelException(
+ "Watermark value '" + storedIndex + "' under
key '" + watermarkKey
+ + "' is not a valid
integer",
+ e);
+ }
while (rawIterator.hasNext() && skipCount <= skipTo) {
rawIterator.next();
skipCount++;
@@ -549,6 +558,15 @@ public class Splitter extends MulticastProcessor {
this.watermarkExpression = watermarkExpression;
}
+ @Override
+ protected void afterSend(ProcessorExchangePair pair, StopWatch watch) {
+ super.afterSend(pair, watch);
+ SplitFailureTracker tracker =
pair.getExchange().getProperty(SPLIT_FAILURE_TRACKER,
SplitFailureTracker.class);
+ if (tracker != null) {
+ tracker.incrementProcessedItems();
+ }
+ }
+
@Override
protected boolean shouldContinueOnFailure(Exchange subExchange, Exchange
original, int index) {
// only honor the tracker when THIS splitter has thresholds configured,
@@ -561,31 +579,43 @@ public class Splitter extends MulticastProcessor {
return super.shouldContinueOnFailure(subExchange, original, index);
}
- // record the failure
- tracker.recordFailure(index, subExchange.getException());
-
- // check if we've exceeded the max failed records
- if (maxFailedRecords > 0 && tracker.getFailureCount() >=
maxFailedRecords) {
+ // a deliberate .stop() or rollback in the child route is not a
failure —
+ // honor the stop/rollback semantics without inflating the failure
count
+ if (subExchange.isRouteStop() || subExchange.isRollbackOnly() ||
subExchange.isRollbackOnlyLast()) {
return false;
}
- // check if we've exceeded the error ratio threshold
- if (errorThreshold > 0) {
- double ratio = (double) tracker.getFailureCount() / (index + 1);
- if (ratio >= errorThreshold) {
+
+ // only record actual unhandled exceptions as failures —
error-handler-handled
+ // exceptions should not inflate the failure count
+ boolean hasException = subExchange.getException() != null;
+ if (hasException) {
+ tracker.recordFailure(index, subExchange.getException());
+
+ // check if we've exceeded the max failed records
+ if (maxFailedRecords > 0 && tracker.getFailureCount() >=
maxFailedRecords) {
return false;
}
- }
+ // check if we've exceeded the error ratio threshold
+ if (errorThreshold > 0) {
+ int processed = tracker.getProcessedItems();
+ if (processed > 0) {
+ double ratio = (double) tracker.getFailureCount() /
processed;
+ if (ratio >= errorThreshold) {
+ return false;
+ }
+ }
+ }
- // Continue processing — clear the exception from the sub-exchange so
that aggregation
- // proceeds normally. The failure is already recorded in the tracker
above, so the
- // SplitResult will still contain the failure details even though the
exception is cleared.
- subExchange.setException(null);
+ // continue processing — clear the exception so aggregation
proceeds normally
+ subExchange.setException(null);
+ }
return true;
}
static final class SplitFailureTracker {
private final AtomicInteger failureCount = new AtomicInteger();
private final AtomicInteger totalItems = new AtomicInteger();
+ private final AtomicInteger processedItems = new AtomicInteger();
private final CopyOnWriteArrayList<SplitResult.Failure> failures = new
CopyOnWriteArrayList<>();
void recordFailure(int index, Exception exception) {
@@ -597,10 +627,18 @@ public class Splitter extends MulticastProcessor {
totalItems.incrementAndGet();
}
+ void incrementProcessedItems() {
+ processedItems.incrementAndGet();
+ }
+
int getTotalItems() {
return totalItems.get();
}
+ int getProcessedItems() {
+ return processedItems.get();
+ }
+
int getFailureCount() {
return failureCount.get();
}
@@ -722,8 +760,9 @@ public class Splitter extends MulticastProcessor {
}
boolean aborted = exchange.getException() != null;
- SplitResult result
- = new SplitResult(tracker.getTotalItems(),
tracker.getFailureCount(), tracker.getFailures(), aborted);
+ SplitResult result = new SplitResult(
+ tracker.getTotalItems(), tracker.getProcessedItems(),
+ tracker.getFailureCount(), tracker.getFailures(), aborted);
exchange.setProperty(ExchangePropertyKey.SPLIT_RESULT, result);
// remove internal tracker
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 a9c8debbb09c..7bc663d7283b 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
@@ -212,6 +212,7 @@ public class AggregateProcessor extends BaseProcessorSupport
completedByStrategy.set(0);
completedByTimeout.set(0);
completedByPredicate.set(0);
+ completedByInterval.set(0);
completedByBatchConsumer.set(0);
completedByForce.set(0);
discarded.set(0);
@@ -404,9 +405,14 @@ public class AggregateProcessor extends
BaseProcessorSupport
long delay = optimisticLockRetryPolicy.getDelay(attempt);
if (delay > 0) {
int nextAttempt = attempt;
- getOptimisticLockingExecutorService().schedule(
- () -> doInOptimisticLock(exchange, key,
callback, nextAttempt, false), delay,
- TimeUnit.MILLISECONDS);
+ getOptimisticLockingExecutorService().schedule(() -> {
+ try {
+ doInOptimisticLock(exchange, key, callback,
nextAttempt, false);
+ } catch (Exception t) {
+ exchange.setException(t);
+ callback.done(false);
+ }
+ }, delay, TimeUnit.MILLISECONDS);
return false;
}
} else {
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/SplitterErrorThresholdTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/SplitterErrorThresholdTest.java
index 48da1eec0ded..c833f759fc54 100644
---
a/core/camel-core/src/test/java/org/apache/camel/processor/SplitterErrorThresholdTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/SplitterErrorThresholdTest.java
@@ -100,8 +100,10 @@ class SplitterErrorThresholdTest extends
ContextTestSupport {
MockEndpoint mock = getMockEndpoint("mock:parallel-split");
mock.expectedMinimumMessageCount(0);
+ // 3 failures out of 5 items = 60% failure rate, exceeds 50% threshold
+ // regardless of parallel completion order
Exchange result = template.send("direct:parallel",
- e -> e.getIn().setBody(Arrays.asList("FAIL", "FAIL", "a", "b",
"c")));
+ e -> e.getIn().setBody(Arrays.asList("FAIL", "FAIL", "FAIL",
"a", "b")));
mock.assertIsSatisfied();
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/SplitterSplitResultTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/SplitterSplitResultTest.java
index d6603f6f3021..0fc517577011 100644
---
a/core/camel-core/src/test/java/org/apache/camel/processor/SplitterSplitResultTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/SplitterSplitResultTest.java
@@ -136,8 +136,9 @@ class SplitterSplitResultTest extends ContextTestSupport {
@Test
void testSplitResultConstructorWithNullFailures() {
- SplitResult result = new SplitResult(5, 0, null, false);
+ SplitResult result = new SplitResult(5, 5, 0, null, false);
assertEquals(5, result.getTotalItems());
+ assertEquals(5, result.getProcessedItems());
assertEquals(0, result.getFailureCount());
assertEquals(5, result.getSuccessCount());
assertFalse(result.isAborted());