shashank created CAMEL-26576:
--------------------------------

             Summary: camel-core - Split/Multicast with shareUnitOfWork: a part 
sent to seda takes the consumer's commit, which then runs with the outcome of 
the seda copy
                 Key: CAMEL-26576
                 URL: https://issues.apache.org/jira/browse/CAMEL-26576
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank
            Assignee: shashank


Found while modelling the shared unit of work of a parallel Split (see the 
linked handover race), then reproduced with deterministic tests on the real 
classes. It does not need parallel processing.

Since Camel 4.14 (CAMEL-22259) the parts of a Split, Multicast or Recipient 
List with {{shareUnitOfWork}} use the parent's unit of work 
({{MulticastProcessor.prepareSharedUnitOfWork}}). The 4.14 upgrade guide says 
the on completions of the parts now run "when the parent UnitOfWork is 
complete", and the Split docs describe {{shareUnitOfWork}} as a way to treat 
the whole message as one unit that either succeeds or fails.

But a part that sends to an endpoint which routes a copy on its own hands over 
the on completions of its unit of work to that copy, for example {{seda}} with 
an {{InOnly}} exchange ({{SedaProducer.addToQueue}} -> 
{{ExchangeHelper.createCorrelatedCopy(exchange, true, true)}} -> 
{{AbstractExchange.handoverCompletions}} -> 
{{DefaultUnitOfWork.handoverSynchronization}}). With the shared unit of work 
that is all the on completions of the parent exchange, including the commit or 
rollback of its consumer ({{GenericFileOnCompletion}}, the SQS delete, ...). 
They then run when that seda copy is done, with the outcome of the copy, not of 
the split:
* a later part fails the split, but the seda copy succeeds: the consumer 
commits. The file is moved to the done folder (not to {{moveFailed}}), an SQS 
message is deleted, so the failed message is not retried;
* the split succeeds, but the seda copy fails: the consumer rolls back (the 
file goes to {{moveFailed}}, the SQS message is redelivered and all the parts 
run again);
* the commit can run before the split has even finished.

Before 4.14 each part had its own unit of work, so a part handed over only its 
own on completions. A part routed into a {{Resequencer}} or 
{{StreamResequencer}} hands over the same way. (The resource exchange of 
{{enrich}} with {{shareUnitOfWork}} has always shared the unit of work of the 
exchange, and can hand over its on completions to a seda copy too, for example 
with {{waitForTaskToComplete=Never}}; that is older and not part of this.)

{code:java}
from("file:inbox?move=done&moveFailed=failed")
    .split(body().tokenize(",")).shareUnitOfWork()
        .choice()
            .when(body().startsWith("a")).to("seda:lines")
            .when(body().isEqualTo("bad")).throwException(new 
IllegalArgumentException("bad line"))
        .end()
    .end();
from("seda:lines").to("log:line");
{code}

A file with {{a,bad}} fails the split, but ends up in {{done}}, not in 
{{failed}}.

On main (1b12eea, 2026-11-02), tests with that route, a Split, a Multicast, a 
Recipient List and a nested Split fail: the consumer on completion runs once, 
as {{onComplete}} for the seda copy instead of {{onFailure}} for the failed 
split exchange; and when only the seda copy fails, {{onFailure}} runs instead 
of {{onComplete}}. The same routes without the seda part, a plain 
{{to("seda:...")}} without a split (handover as designed, see CAMEL-9291), and 
a split where all parts succeed behave as expected.

Related: CAMEL-22259 (the shared unit of work, 4.14), CAMEL-9291 (handover to 
seda from a plain route is by design), CAMEL-25094 (the stream cache of a seda 
copy sent from a Split/Multicast). Affects 4.14.0 and later.

*Proposal*
* A part that shares the unit of work of the parent exchange 
({{prepareSharedUnitOfWork}}) hands over only the on completions it added to 
its own exchange, as before 4.14. The on completions of the parent (such as its 
consumer's commit) stay with the parent and run when the split is done, with 
its outcome. On completions added to the unit of work directly (such as by a 
route {{onCompletion}}) stay with the parent too.
* A new {{ExchangeExtension.setSharedUnitOfWork(UnitOfWork)}} (default: 
{{setUnitOfWork}}) marks the exchange; it remembers the on completions it adds 
while it uses that unit of work and hands over only those 
({{UnitOfWork.handoverSynchronization(target, filter)}}). A plain InOnly send 
to seda outside a split is unchanged.
* Upgrade guide entry: the consumer now commits when the split is done, 
possibly before the seda copies are processed, so a copy that still reads the 
input (such as a file body not loaded yet) must load it first or wait for the 
copy ({{waitForTaskToComplete=Always}}).
* Target main. A backport to 4.18.x (and 4.14.x) is possible as is (the method 
is a default method on the internal {{ExchangeExtension}}), but it changes when 
the consumer commits, so that is the committers' call.

_Filed with Claude Code on behalf of allthingssecurity._



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

Reply via email to