shashank created CAMEL-25011:
--------------------------------

             Summary: Saga EIP with the in-memory saga service: split, 
multicast, recipient list and wire tap sub-exchanges lose the saga (MANDATORY 
fails, REQUIRED completes independent sagas)
                 Key: CAMEL-25011
                 URL: https://issues.apache.org/jira/browse/CAMEL-25011
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank


Since CAMEL-23469 the saga id travels in the exchange extension 
({{getSagaLongRunningAction}}). {{AbstractExchange(AbstractExchange parent)}}, 
used by {{Exchange.copy()}}, does not copy that field 
({{AbstractExchange.java:123-159}}). Only {{ExchangeHelper.copyResults}} copies 
it ({{ExchangeHelper.java:396}}). Sub-exchanges of split, multicast, recipient 
list and wire tap are created with {{copy()}}/{{createCorrelatedCopy}}, so they 
have no internal saga id.

This used to be covered by the {{Long-Running-Action}} header, which is copied 
with the message. Since CAMEL-24449 (4.22.1, 4.18.5, 4.23.0) 
{{SagaProcessor.getCurrentSagaCoordinator}} only reads the header when 
{{sagaService.isLongRunningActionHeaderSupported()}} 
({{SagaProcessor.java:57-64}}), which is false for {{InMemorySagaService}}. The 
combination means a saga step reached through split/multicast/recipient 
list/wire tap no longer sees the saga:

* {{MANDATORY}}: fails with "Exchange is not part of a saga";
* {{REQUIRED}}: each sub-exchange starts its own saga and completes it, even 
when the parent saga compensates;
* {{SUPPORTS}}: runs outside any saga.

*Reproduction*:
{code:java}
from("direct:owner").saga().compensation("direct:compOwner")
    .split(body()).to("direct:item").end()
    .process(e -> { throw new IllegalStateException("payment declined"); });
from("direct:item").saga().propagation(MANDATORY /* or REQUIRED */)
    .compensation("direct:compItem").completion("direct:complItem")
    .process(e -> reserved++);
{code}
Body {{List.of("a","b","c")}}:
{noformat}
[copy MANDATORY] owner exception chain=[CamelExchangeException: Exchange is not 
part of a saga. Exchange[]]
[copy MANDATORY] itemAction=0 compOwner=1 compItem=0 complItem=0
[copy REQUIRED]  owner exception chain=[IllegalStateException: payment declined]
[copy REQUIRED]  itemAction=3 compOwner=1 compItem=0 complItem=3
[copy MANDATORY+headerSupport] owner exception chain=[IllegalStateException: 
payment declined]
[copy MANDATORY+headerSupport] itemAction=3 compOwner=1 compItem=3 complItem=0
{noformat}
The last run uses an {{InMemorySagaService}} subclass that returns {{true}} 
from {{isLongRunningActionHeaderSupported()}}, i.e. the behaviour before 
CAMEL-24449: the items join the saga and are compensated. With REQUIRED the 
three reservations are confirmed ({{complItem=3}}) although the order was 
compensated.

The TLA+ saga model treats "participant cannot see S" like the stale-id case: 
{{NoMixedOutcome}} is violated for REQUIRED and SUPPORTS.

*Proposed fix:* copy the internal saga id with the exchange: set 
{{this.sagaLongRunningAction = parent.sagaLongRunningAction}} in the 
{{AbstractExchange}} copy constructor (and in {{ExtendedExchangeExtension}}'s 
copy path if it has one), so every copy keeps the saga it was created in, 
independent of the header. That keeps CAMEL-24449's intent (a message cannot 
pick a saga through the header) because the internal field is only ever set by 
Camel. Add a test with split + MANDATORY and multicast + REQUIRED under 
{{InMemorySagaService}}.

The impact is wider than sub-exchanges running saga steps: 
{{ExchangeHelper.copyResults}} copies the saga id from a sub-exchange back to 
the original exchange, and the copy has none, so it clears it. After a 
multicast (default aggregation), recipient list, routing slip, failover load 
balancer or loop with copy, the *original* exchange has lost its saga: a 
following MANDATORY step fails, and a REQUIRED step silently starts a saga of 
its own. The MANUAL completion example in the saga EIP docs 
({{seda:operationCompleted}} with MANDATORY, then {{saga:complete}}) is 
affected too, as the seda consumer gets a copy.

_Filed with Claude Code on behalf of allthingssecurity._




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to