You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@camel.apache.org by "shashank (Jira)" <ji...@apache.org> on 2026/09/23 13:12:00 UTC
[jira] [Created] (CAMEL-24943) Aggregate EIP: the recover task can re-deliver an aggregated exchange that is being completed normally, which sends it twice
shashank created CAMEL-24943:
--------------------------------
Summary: 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
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)