[
https://issues.apache.org/jira/browse/CAMEL-24943?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen resolved CAMEL-24943.
---------------------------------
Resolution: Fixed
Merged to main for 4.23.0 via https://github.com/apache/camel/pull/26788
(commit fe67aa8989d8196c7eb944d9cb61ba07a84fe43c).
_Claude Code on behalf of davsclaus_
> Aggregate EIP: the recover task can re-deliver an aggregated exchange that is
> being completed normally, which sends it twice
> ----------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-24943
> URL: https://issues.apache.org/jira/browse/CAMEL-24943
> Project: Camel
> Issue Type: Bug
> Components: camel-core
> Reporter: shashank
> Priority: Minor
> Fix For: 4.23.0
>
>
> With a {{RecoverableAggregationRepository}} (JDBC, Redis, Hazelcast,
> Infinispan, {{KeyValueAggregationRepository}}, ...) an aggregated exchange
> can be sent twice even though nothing failed: once by the thread that
> completed the group and once by the recover task.
> When an incoming exchange completes a group, {{AggregateProcessor}} does the
> following:
> # Under the aggregation lock, {{onCompletion}} calls
> {{aggregationRepository.remove(...)}}. A recoverable repository moves the
> exchange to its recovery (completed) store there.
> # The lock is released.
> # {{onSubmitCompletion}} adds the exchange id to
> {{inProgressCompleteExchanges}} and sends the exchange.
> If the recover task scans the repository and checks the id between steps 2
> and 3, the id is not "in progress", so the task recovers the exchange and
> submits it. Then the completing thread submits it as well.
> Observed with {{completionSize(2)}}, recovery interval 100 ms, and the
> completing thread paused right after it releases the aggregation lock:
> {noformat}
> producer paused after unlock; completed store =
> [2A3CFA493BBF252-0000000000000001], in progress = 0
> recover task re-delivered the exchange while producer paused: true
> A+B id=2A3CFA493BBF252-0000000000000001 redelivered=true
> A+B id=2A3CFA493BBF252-0000000000000001 redelivered=null
> aggregated exchange delivered 2 times (expected 1)
> {noformat}
> Without the pause the exchange is delivered once. The same happens with
> {{KeyValueAggregationRepository}} with its default settings.
> With {{optimisticLocking}} the aggregation lock is a {{NoLock}}, so the
> recover task is not serialized with any completion path. A group completed by
> {{completionTimeout}} is also delivered twice if the recover task runs
> between the timeout checker's {{remove}} and its {{onSubmitCompletion}}:
> {noformat}
> A+B id=CCDAE062CAC9981-0000000000000001 redelivered=true completedBy=timeout
> A+B id=CCDAE062CAC9981-0000000000000001 redelivered=null completedBy=timeout
> {noformat}
> This is a remaining case of the race addressed by CAMEL-6097 and CAMEL-8010.
> Those fixes cover exchanges that are registered as in progress before or
> during the scan, but not an exchange that is already in the recovery store
> and not yet registered.
> The pessimistic window can also be reproduced without pausing anything inside
> Camel's own code: with {{completionFromBatchConsumer}} several groups are
> completed by one exchange, and the others wait in the completed store,
> unregistered, while the first one is submitted.
> Proposed fix: in {{onCompletion}}, before calling
> {{aggregationRepository.remove}} on a recoverable repository, mark the
> aggregated exchange as "being completed". {{onSubmitCompletion}} removes the
> mark once it has registered the id in {{inProgressCompleteExchanges}}, and
> the recover task treats a marked id as in progress. The mark is removed again
> when the exchange is not submitted: {{remove}} throws, the exchange is
> discarded, the aggregation fails after a group was completed, or the group is
> force discarded. Those exchanges are then still recovered as before.
> The mark is kept in a separate map with a counter per exchange id instead of
> going straight into {{inProgressCompleteExchanges}}. With optimistic locking,
> two threads can try to complete the same group. The one whose compare-and-set
> remove fails must not clear the registration of the one that succeeded.
> Delivery stays at least once: every marked exchange is either submitted, and
> then covered by the existing in-progress and {{onFailure}} handling, or
> unmarked so the recover task picks it up. The change only prevents the
> recover task from sending an exchange that is about to be sent anyway.
> This does not cover several Camel instances sharing one repository. The
> recover task of another instance can still pick up the exchange; the JDBC
> repository's {{recoveryByInstance}} option exists for that case.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)