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

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