shashank created CAMEL-25017:
--------------------------------
Summary: Claim Check EIP: Split/Multicast/Recipient List/Wire
Tap/SEDA copies share the parent's claim check repository, so parallel parts
get each other's messages back
Key: CAMEL-25017
URL: https://issues.apache.org/jira/browse/CAMEL-25017
Project: Camel
Issue Type: Bug
Components: camel-core
Reporter: shashank
{{ClaimCheckProcessor}} keeps its repository in the exchange property
{{CamelClaimCheckRepository}} and creates it lazily
({{ClaimCheckProcessor.java:212-219}}). {{DefaultClaimCheckRepository}} is a
plain {{HashMap}} plus an {{ArrayDeque}}. The exchange copy constructor copies
the internal properties by reference ({{AbstractExchange.java:126}}, a new
EnumMap of the parent's internal properties), so once the parent exchange has
used the Claim Check, every copy made by Split, Multicast, Recipient List, Wire
Tap, Enrich, ... uses the same repository instance as the parent and as each
other.
Consequences:
* with parallel processing, parts that {{Set}}/{{Get}} the same key overwrite
each other's data, and {{Push}}/{{Pop}} pop another part's message: a part
continues with another part's body and headers, silently;
* the unsynchronized {{HashMap}}/{{ArrayDeque}} are modified concurrently
(parallel parts, or a Wire Tap copy running next to the original);
* also sequentially: a part that fails between {{Push}} and {{Pop}} leaves its
message on the parent's stack, and the parent's later {{Pop}} returns the
part's message instead of its own.
A typical route that is affected: {{claimCheck(Set, "original")}} ->
{{split(...).parallelProcessing()}} -> in each part {{claimCheck(Set, "item")}}
/ call a service / {{claimCheck(Get, "item")}} -> after the split
{{claimCheck(Get, "original")}}.
*Reproduction*: split "A,B" with {{parallelProcessing()}}, each part saves its
message ({{Set "item"}} or {{Push}}), a service step sets the body to
"reply-from-service", and restores it ({{Get "item"}} or {{Pop}}); latches
force the order part 0 saves, part 1 saves, part 0 restores, part 1 restores.
20 runs each:
{noformat}
setget parent did not use the Claim Check: 0/20 wrong (always [0:A,
1:B])
setget parent did claimCheck(Set,"orig") before split: 20/20 wrong (e.g. [0:B,
1:B], expected [0:A, 1:B])
pushpop parent did not use the Claim Check: 0/20 wrong
pushpop parent did claimCheck(Set,"orig") before split: 20/20 wrong (e.g. [0:B,
1:A])
{noformat}
The only difference between the two routes is one {{claimCheck(Set, "orig")}}
in the parent before the split.
A TLA+ model with a shared repository violates "each part gets its own message
back" in 5 states for Set/Get and for Push/Pop; with a repository per exchange
it holds.
This contradicts the {{ClaimCheckProcessor}} javadoc, which says the repository
is "not shared among Exchanges, but a private instance is created per
Exchange". Sharing only happens when the parent used the Claim Check before the
EIP, because the repository is created on first use.
*Proposed fix:* give each copy its own copy of the repository
({{DefaultClaimCheckRepository}} implements {{SafeCopyProperty}}, applied in
{{AbstractExchange.copy()}}, {{ExchangeHelper.copyExchangeWithProperties}}
(Disruptor) and the deprecated {{PooledProcessorExchangeFactory}}), so a part
can still read what the parent saved but its own writes stay private. The
exchange that {{Set}}/{{Push}} stores should not carry the repository; detach
it while making that copy, so the repository is not copied on every
{{Set}}/{{Push}}. (Giving copies an empty repository instead breaks
{{MulticastMixOriginalMessageBodyAndEnrichedHeadersClaimCheckTest}}, where a
multicast part reads the parent's claim check.)
Behaviour change: what a part stores is no longer visible to the parent or to
the other parts. After a Multicast or Recipient List, the parent continues with
the repository of the result exchange, as for its other properties.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)