You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@camel.apache.org by "shashank (Jira)" <ji...@apache.org> on 2026/09/25 07:44:00 UTC
[jira] [Created] (CAMEL-25017) 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
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)