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)

Reply via email to