shashank created CAMEL-26309:
--------------------------------
Summary: camel-core - Aggregate EIP: when the thread pool rejects
an aggregated exchange, the completed group is lost (not recovered, never
confirmed) and stays counted as in progress
Key: CAMEL-26309
URL: https://issues.apache.org/jira/browse/CAMEL-26309
Project: Camel
Issue Type: Bug
Components: camel-core
Reporter: shashank
Assignee: shashank
When the thread pool of the Aggregate EIP rejects the task that sends an
aggregated exchange, the completed group is lost and the aggregator keeps
counting it as in progress forever.
{{AggregateProcessor.onSubmitCompletion}} (camel-core-processor, main
c536c99caaaa) first books the aggregated exchange as in progress
({{inProgressCount.increment()}}, {{inProgressCompleteExchanges.add(id)}}),
calls {{AggregationStrategy.onCompletion}}, adds the {{AggregateOnCompletion}}
that undoes the booking, and only then calls {{executorService.execute(...)}}.
The group has already been removed from the aggregation repository
({{onCompletion}}). If {{execute}} throws {{RejectedExecutionException}} (an
{{executorService}} or thread pool profile with {{rejectedPolicy=Abort}} and a
bounded queue; the Abort handler throws for a task that is not {{Rejectable}},
and the aggregator's task is a lambda), nothing runs the on completions of the
aggregated exchange:
# the in progress booking is never undone: {{getInProgressCompleteExchanges()}}
stays above 0, and with {{forceCompletionOnStop}} or {{completeAllOnStop}} a
later route or CamelContext stop waits in {{inProgressCount.await()}} (no
timeout) until the shutdown strategy times out;
# with a recoverable repository (JDBC, LevelDB, Cassandra, Infinispan, the
key-value one ...) the group stays in the completed store, but the recover task
skips it because its id stays in {{inProgressCompleteExchanges}}: it is never
recovered until the application restarts;
# with the memory repository the group (all its messages but the last) is gone,
and nothing says so: the exchange that completed the group fails with a
{{RejectedExecutionException}} that does not mention the group;
# on completions that the aggregation strategy added to the aggregated exchange
(the zip and tar strategies delete their temporary file there), and the release
of spooled stream caches the group held, never run.
The same happens when the group is completed by the completion timeout, the
interval, a force completion or the recover task (the exception is logged or
retried there, but the group is already removed). When the batch consumer
completes several groups at once ({{completionFromBatchConsumer}}), the groups
after the rejected one are not even submitted: they have been removed from the
repository too and are lost the same way, and with a recoverable repository
they stay marked as being completed, so the recover task skips them as well.
Reproduced on main with a route test (latches, no sleeps):
{{aggregate(header("id"),
strategy).completionSize(2).executorService("oneThread")}} with a profile of 1
thread, queue size 1, {{rejectedPolicy=Abort}}; group A blocks the thread,
group B waits in the queue, group C is rejected. After the thread is released,
A and B are delivered; C is neither delivered nor passed to the exception
handler, its on completion does not run, and
{{getInProgressCompleteExchanges()}} stays 1. With a
{{KeyValueAggregationRepository}} with recovery every 100 ms, C is never
recovered. With {{completionFromBatchConsumer}} and a batch that completes the
groups C, D and E at once, none of them is passed to the exception handler, and
with the recoverable repository neither C nor D is recovered.
The existing {{AggregateParallelThreadPoolAbortTest}} expects the caller to
fail when the pool rejects; that is kept.
*Fix*: when {{execute}} is rejected, fail the aggregated exchange as if its
processing had failed: set the {{RejectedExecutionException}} on it, run its on
completions ({{AggregateOnCompletion.onFailure}} undoes the in progress booking
and does not confirm the exchange, so a recoverable repository recovers it; the
strategy's and the stream cache on completions run), pass it to the
aggregator's exception handler (logged at WARN with the aggregated exchange by
default), and rethrow so the caller fails as before. The other groups of a
batch are still sent (or failed the same way when rejected too) before the
first rejection is rethrown. A recovered exchange that the pool rejects again
is recovered by a later scan; the attempt counts as a redelivery, so
{{maximumRedeliveries}} and {{deadLetterUri}} apply.
Not changed: the default (synchronous) executor never rejects. With Camel's
{{Block}} policy {{execute}} throws only when the pool is shut down or the
waiting caller is interrupted (handled the same way). The JDK
{{CallerRunsPolicy}} behind Camel's {{CallerRuns}} runs the task in the caller,
but drops it without an exception when the pool is shut down; that case is not
covered here.
*Tests*: new {{AggregateRejectedCompletionTest}} (camel-core; latches, no
sleeps; the setup above): {{testRejectedAggregatedExchangeIsFailed}},
{{testRejectedAggregatedExchangeIsRecovered}},
{{testRejectedBatchCompletionFailsEveryGroup}} and
{{testRejectedBatchCompletionIsRecovered}} fail on main (3 of 3 runs) and pass
with the fix. The camel-core tests under {{org/apache/camel/processor}} and the
other aggregator tests ({{*Aggregat*Test}}): 2723 tests, 0 failures, including
{{AggregateParallelThreadPoolAbortTest}} and
{{AggregateParallelThreadPoolCallerRunsTest}}.
*How it was found*: by reading the EIPs that hand work to a thread pool
(Threads, WireTap and Multicast handle a rejection, the aggregator does not)
while writing drivers for a JVM model in Rocq that runs the real class files
with a thread layer and {{ThreadPoolExecutor}} with its rejection policies;
reproduced on HotSpot with the test above. A kernel-checked theorem on the
class files of main states the leak (after a rejected completion and a drained
pool the aggregator still counts the exchange as in progress), and one on the
fix states that the exchange is failed and passed to the exception handler.
*Affected*: main, camel-4.22.x, camel-4.18.x and camel-4.14.x have the same
{{onSubmitCompletion}} (read, not run).
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)