[ 
https://issues.apache.org/jira/browse/CAMEL-26309?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Work on CAMEL-26309 started by shashank.
----------------------------------------
> 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
>            Priority: Major
>
> 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)

Reply via email to