You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@camel.apache.org by "Claus Ibsen (Jira)" <ji...@apache.org> on 2026/09/28 06:58:00 UTC

[jira] [Resolved] (CAMEL-25011) 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)

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

Claus Ibsen resolved CAMEL-25011.
---------------------------------
    Resolution: Fixed

Merged to main in https://github.com/apache/camel/pull/26869 (83c20f5550d3). Labelled for backport to 4.22.x and 4.18.x.

_Claude Code on behalf of davsclaus_

> 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
>            Priority: Minor
>             Fix For: 4.23.0
>
>
> 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)