shashank created CAMEL-26569:
--------------------------------

             Summary: camel-core - parallel Split/Multicast with 
shareUnitOfWork: on completion handover fails with 
ConcurrentModificationException
                 Key: CAMEL-26569
                 URL: https://issues.apache.org/jira/browse/CAMEL-26569
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank
            Assignee: shashank


Found with a SPIN model of {{DefaultUnitOfWork}} shared by the parts of a 
parallel Split (two parts handing over, one part adding an on completion, then 
the parent's {{done()}}), then reproduced with a deterministic test on the real 
classes.

Since Camel 4.14 the parts of a Split, Multicast or Recipient List with 
{{shareUnitOfWork}} use the parent's unit of work 
({{MulticastProcessor.prepareSharedUnitOfWork}}, see the 4.14 upgrade guide). 
With {{parallelProcessing}} they use it at the same time. 
{{DefaultUnitOfWork.addSynchronization}} / {{removeSynchronization}} take the 
unit of work lock, but {{handoverSynchronization}} iterates the list and 
removes from it with an iterator, without the lock. A part hands over the on 
completions whenever it sends to an endpoint that routes a copy on its own, for 
example a {{seda}} endpoint with an {{InOnly}} exchange 
({{SedaProducer.addToQueue}} -> {{ExchangeHelper.createCorrelatedCopy(exchange, 
true, true)}} -> {{handoverCompletions}}). {{Resequencer}} and 
{{StreamResequencer}} hand over the same way.

So when one part hands over while another part adds an on completion, or hands 
over too:
* the part fails with a {{ConcurrentModificationException}} from 
{{it.remove()}}; with {{shareUnitOfWork}} that fails the whole split, and the 
copy was never queued;
* an on completion is handed over to two copies and runs twice (for example the 
consumer's commit or file move), or, when the part that took it fails, it is 
never run.

{code:java}
from("file:inbox")   // or any consumer that adds an on completion
    .split(body().tokenize("\n")).parallelProcessing().shareUnitOfWork()
        .to("seda:lines")
    .end();
{code}

{noformat}
java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java)
    at java.base/java.util.ArrayList$Itr.remove(ArrayList.java)
    at 
org.apache.camel.impl.engine.DefaultUnitOfWork.handoverSynchronization(DefaultUnitOfWork.java:258)
    at 
org.apache.camel.support.AbstractExchange.handoverCompletions(AbstractExchange.java:863)
    at 
org.apache.camel.support.ExchangeHelper.createCorrelatedCopy(ExchangeHelper.java:320)
    at 
org.apache.camel.component.seda.SedaProducer.prepareCopy(SedaProducer.java:209)
{noformat}

SPIN trace (short): parts 1 and 2 both create an iterator over [consumerSync]; 
part 3 adds partSync (under the lock, which the iterators ignore); part 2 hands 
consumerSync to its copy and removes it; part 1 gets a CME, its copy is dropped 
and the split fails. In another trace part 1 had already taken consumerSync 
into its copy, so it is never run. The fixed model (copy under the lock, then 
take each synchronization out under the lock before handing it over) holds, 
with a witness that the good path is reachable.

On main (1b12eea, 2026-11-02) a deterministic test fails: part A is held inside 
the handover (in {{allowHandover()}} of the on completion) while part B adds an 
on completion, and two parts are held there at the same time; both splits fail 
with the CME, while the same route without {{parallelProcessing}} passes. 
Without any hook, 2000 splits of 4 parts each sending to {{seda}} (counted 
after a 60 second wait) failed 4 times with the CME and ran the consumer on 
completion twice once.

Related: CAMEL-22259 (the shared unit of work, 4.14), CAMEL-22533 and 
CAMEL-23681 (PR #23777) (a race with the same setup in the route stack of the 
shared unit of work, now under the same lock), CAMEL-16566 (an earlier 
single-threaded CME in {{handoverSynchronization}}). Affects 4.14.0 and later; 
fix for 4.24.0 and 4.18.6 (the same code is on 4.18.x).

*Proposal*
* {{handoverSynchronization}}: take a copy of the list under the lock, decide 
on the copy ({{allowHandover}}, filter), and take each synchronization out of 
the list under the lock before handing it over, skipping one another thread has 
already taken, so it is handed over once. The target is not called while 
holding the lock (no lock order between two units of work).
* {{done()}}, {{beforeRoute()}}, {{afterRoute()}}: run the synchronizations 
from a copy taken under the lock (they copy the list without it, so a 
concurrent add or handover can leave a {{null}} in the copy). No lock when 
there are no synchronizations (hot path, CAMEL-25132).
* For a single thread the order and the result stay the same; an on completion 
added during the handover is no longer visited (that was the CME of 
CAMEL-16566).
* Not part of this: after a parallel split times out, a late part can still 
hand over or add an on completion while {{done()}} runs (it can then run twice, 
or not at all).
* Not part of this (could be a separate JIRA): with {{shareUnitOfWork}}, a part 
that sends to {{seda}} hands over all the on completions of the shared unit of 
work (including the consumer's commit or file move) to its copy, so they run 
when that copy is done, not when the split is done (also without parallel 
processing).

_Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to