This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new ab21f790a87d CAMEL-25017: camel-core - Claim Check EIP: exchange
copies must not share the claim check repository (#26882)
ab21f790a87d is described below
commit ab21f790a87d3483f6be23dd4b8150a8dc62e3e4
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Sep 28 13:39:35 2026 +0530
CAMEL-25017: camel-core - Claim Check EIP: exchange copies must not share
the claim check repository (#26882)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../engine/PooledProcessorExchangeFactory.java | 12 ++
.../camel/processor/ClaimCheckProcessor.java | 22 +++-
.../processor/DefaultClaimCheckRepository.java | 26 +++-
.../processor/ClaimCheckEipSplitParallelTest.java | 146 +++++++++++++++++++++
.../processor/ClaimCheckEipStoreCopyTest.java | 85 ++++++++++++
...oledExchangeClaimCheckEipSplitParallelTest.java | 38 ++++++
.../org/apache/camel/util/ExchangeHelperTest.java | 22 ++++
.../org/apache/camel/support/AbstractExchange.java | 3 +
.../org/apache/camel/support/ExchangeHelper.java | 10 ++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 14 ++
10 files changed, 370 insertions(+), 8 deletions(-)
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/PooledProcessorExchangeFactory.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/PooledProcessorExchangeFactory.java
index 17c1511faf8f..2a38eecff745 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/PooledProcessorExchangeFactory.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/PooledProcessorExchangeFactory.java
@@ -22,6 +22,7 @@ import org.apache.camel.ExchangePattern;
import org.apache.camel.ExchangePropertyKey;
import org.apache.camel.PooledExchange;
import org.apache.camel.Processor;
+import org.apache.camel.SafeCopyProperty;
import org.apache.camel.spi.ProcessorExchangeFactory;
import org.apache.camel.support.DefaultPooledExchange;
import org.apache.camel.support.ExchangeHelper;
@@ -78,6 +79,7 @@ public class PooledProcessorExchangeFactory extends
PrototypeProcessorExchangeFa
// reset the copy's clock for reuse
((ResetableClock) answer.getClock()).reset();
ExchangeHelper.copyResults(answer, exchange);
+ safeCopyClaimCheckRepository(answer);
return answer;
}
@@ -103,6 +105,7 @@ public class PooledProcessorExchangeFactory extends
PrototypeProcessorExchangeFa
((ResetableClock) answer.getClock()).reset();
ExchangeHelper.copyResults(answer, exchange);
+ safeCopyClaimCheckRepository(answer);
// do not reuse message id on copy
answer.getIn().setMessageId(null);
if (handover) {
@@ -114,6 +117,15 @@ public class PooledProcessorExchangeFactory extends
PrototypeProcessorExchangeFa
return answer;
}
+ private static void safeCopyClaimCheckRepository(Exchange copy) {
+ // the claim check repository is scoped per exchange, so the copy must
not share it
+ // (the same as Exchange.copy() does)
+ Object repo =
copy.getProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY);
+ if (repo instanceof SafeCopyProperty scp) {
+ copy.setProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY,
scp.safeCopy());
+ }
+ }
+
@Override
public Exchange create(Endpoint fromEndpoint, ExchangePattern
exchangePattern) {
Exchange answer = pool.poll();
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/ClaimCheckProcessor.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/ClaimCheckProcessor.java
index a0b38b27925b..25fa9a7880b6 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/ClaimCheckProcessor.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/ClaimCheckProcessor.java
@@ -167,8 +167,7 @@ public class ClaimCheckProcessor extends
BaseProcessorSupport implements IdAware
}
private static void operationPushHandler(Exchange exchange, String
claimKey, ClaimCheckRepository repo) {
- // copy exchange, and do not share the unit of work
- Exchange copy = ExchangeHelper.createCorrelatedCopy(exchange, false);
+ Exchange copy = createClaimCheckCopy(exchange);
LOG.debug("Push: {} -> {}", claimKey, copy);
repo.push(copy);
}
@@ -198,9 +197,7 @@ public class ClaimCheckProcessor extends
BaseProcessorSupport implements IdAware
}
private static void operationSetHandler(Exchange exchange, String
claimKey, ClaimCheckRepository repo) {
-
- // copy exchange, and do not share the unit of work
- Exchange copy = ExchangeHelper.createCorrelatedCopy(exchange, false);
+ Exchange copy = createClaimCheckCopy(exchange);
boolean addedNew = repo.add(claimKey, copy);
if (addedNew) {
LOG.debug("Add: {} -> {}", claimKey, copy);
@@ -209,6 +206,21 @@ public class ClaimCheckProcessor extends
BaseProcessorSupport implements IdAware
}
}
+ private static Exchange createClaimCheckCopy(Exchange exchange) {
+ // the stored copy must not carry the repository: it does not need it,
and it must not replace the repository of
+ // the exchange when an aggregation strategy returns the stored copy
as the result. So detach the repository
+ // while copying, instead of copying it (which would copy all its
claim checks on every Set or Push)
+ Object repo =
exchange.removeProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY);
+ try {
+ // copy exchange, and do not share the unit of work
+ return ExchangeHelper.createCorrelatedCopy(exchange, false);
+ } finally {
+ if (repo != null) {
+
exchange.setProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY, repo);
+ }
+ }
+ }
+
private static ClaimCheckRepository getClaimCheckRepository(Exchange
exchange) {
ClaimCheckRepository repo
=
exchange.getProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY,
ClaimCheckRepository.class);
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/DefaultClaimCheckRepository.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/DefaultClaimCheckRepository.java
index d86b8bf18991..2056df18e1c2 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/DefaultClaimCheckRepository.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/DefaultClaimCheckRepository.java
@@ -22,15 +22,35 @@ import java.util.HashMap;
import java.util.Map;
import org.apache.camel.Exchange;
+import org.apache.camel.SafeCopyProperty;
import org.apache.camel.spi.ClaimCheckRepository;
/**
* The default {@link ClaimCheckRepository} implementation that is an
in-memory storage.
+ * <p/>
+ * The repository is not thread-safe, as it is scoped per exchange. When an
exchange is copied (such as by the Splitter,
+ * Multicast, Recipient List or Wire Tap EIPs), then the copy gets its own
repository, which starts with the same claim
+ * checks, via {@link #safeCopy()}.
*/
-public class DefaultClaimCheckRepository implements ClaimCheckRepository {
+public class DefaultClaimCheckRepository implements ClaimCheckRepository,
SafeCopyProperty {
- private final Map<String, Exchange> map = new HashMap<>();
- private final Deque<Exchange> stack = new ArrayDeque<>();
+ private final Map<String, Exchange> map;
+ private final Deque<Exchange> stack;
+
+ public DefaultClaimCheckRepository() {
+ this.map = new HashMap<>();
+ this.stack = new ArrayDeque<>();
+ }
+
+ private DefaultClaimCheckRepository(DefaultClaimCheckRepository source) {
+ this.map = new HashMap<>(source.map);
+ this.stack = new ArrayDeque<>(source.stack);
+ }
+
+ @Override
+ public DefaultClaimCheckRepository safeCopy() {
+ return new DefaultClaimCheckRepository(this);
+ }
@Override
public boolean add(String key, Exchange exchange) {
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipSplitParallelTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipSplitParallelTest.java
new file mode 100644
index 000000000000..ba9d9c1e0b1b
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipSplitParallelTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.processor;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.ClaimCheckOperation;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The claim check repository is scoped per exchange: the exchanges created by
a parallel Splitter get their own copy of
+ * the parent's claim checks, and must not get the message of another part
back.
+ */
+class ClaimCheckEipSplitParallelTest extends ContextTestSupport {
+
+ private final CountDownLatch part0Saved = new CountDownLatch(1);
+ private final CountDownLatch part1Saved = new CountDownLatch(1);
+ private final CountDownLatch part0Restored = new CountDownLatch(1);
+
+ @Test
+ void testSetGetInParallelSplit() throws Exception {
+ assertEachPartRestoresItsOwnMessage("direct:setget");
+ }
+
+ @Test
+ void testPushPopInParallelSplit() throws Exception {
+ assertEachPartRestoresItsOwnMessage("direct:pushpop");
+ }
+
+ @Test
+ void testPartGetsClaimCheckOfParent() throws Exception {
+ getMockEndpoint("mock:part").expectedBodiesReceived("A,B", "A,B");
+ getMockEndpoint("mock:result").expectedBodiesReceived("A,B");
+
+ template.sendBody("direct:parent", "A,B");
+
+ assertMockEndpointsSatisfied();
+ }
+
+ private void assertEachPartRestoresItsOwnMessage(String uri) throws
Exception {
+ getMockEndpoint("mock:part").expectedBodiesReceivedInAnyOrder("0:A",
"1:B");
+ getMockEndpoint("mock:result").expectedBodiesReceived("A,B");
+
+ template.sendBody(uri, "A,B");
+
+ assertMockEndpointsSatisfied();
+ }
+
+ // the parts save their message in this order: part 0, part 1, and then
restore it: part 0, part 1
+
+ private void beforeSave(Exchange exchange) throws Exception {
+ if (splitIndex(exchange) == 1) {
+ await(part0Saved);
+ }
+ }
+
+ private void afterSave(Exchange exchange) throws Exception {
+ if (splitIndex(exchange) == 0) {
+ part0Saved.countDown();
+ await(part1Saved);
+ } else {
+ part1Saved.countDown();
+ await(part0Restored);
+ }
+ }
+
+ private void afterRestore(Exchange exchange) {
+ if (splitIndex(exchange) == 0) {
+ part0Restored.countDown();
+ }
+ }
+
+ private static int splitIndex(Exchange exchange) {
+ return exchange.getProperty(Exchange.SPLIT_INDEX, Integer.class);
+ }
+
+ private static void await(CountDownLatch latch) throws
InterruptedException {
+ assertTrue(latch.await(10, TimeUnit.SECONDS), "The other part did not
reach the expected step");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:setget")
+ .claimCheck(ClaimCheckOperation.Set, "original")
+ .split(body().tokenize(",")).parallelProcessing()
+ .process(e -> beforeSave(e))
+ .claimCheck(ClaimCheckOperation.Set, "item")
+ .process(e -> afterSave(e))
+ .setBody(constant("reply-from-service"))
+ .claimCheck(ClaimCheckOperation.Get, "item")
+ .process(e -> afterRestore(e))
+
.setBody(simple("${exchangeProperty.CamelSplitIndex}:${body}"))
+ .to("mock:part")
+ .end()
+ .claimCheck(ClaimCheckOperation.Get, "original")
+ .to("mock:result");
+
+ from("direct:pushpop")
+ .claimCheck(ClaimCheckOperation.Set, "original")
+ .split(body().tokenize(",")).parallelProcessing()
+ .process(e -> beforeSave(e))
+ .claimCheck(ClaimCheckOperation.Push)
+ .process(e -> afterSave(e))
+ .setBody(constant("reply-from-service"))
+ .claimCheck(ClaimCheckOperation.Pop)
+ .process(e -> afterRestore(e))
+
.setBody(simple("${exchangeProperty.CamelSplitIndex}:${body}"))
+ .to("mock:part")
+ .end()
+ .claimCheck(ClaimCheckOperation.Get, "original")
+ .to("mock:result");
+
+ from("direct:parent")
+ .claimCheck(ClaimCheckOperation.Set, "original")
+ .split(body().tokenize(","))
+ .claimCheck(ClaimCheckOperation.Get, "original")
+ .to("mock:part")
+ .end()
+ .to("mock:result");
+ }
+ };
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipStoreCopyTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipStoreCopyTest.java
new file mode 100644
index 000000000000..e74fc4e31c43
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/ClaimCheckEipStoreCopyTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.processor;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.ExchangePropertyKey;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.ClaimCheckOperation;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The copy of the exchange that Set and Push store does not carry the claim
check repository, and making it must not
+ * copy the repository (which would copy all its claim checks on every Set or
Push).
+ */
+class ClaimCheckEipStoreCopyTest extends ContextTestSupport {
+
+ private final CountingClaimCheckRepository repository = new
CountingClaimCheckRepository();
+ private final AtomicInteger safeCopiesBeforeMock = new AtomicInteger(-1);
+ private final AtomicReference<Object> repositoryAtEnd = new
AtomicReference<>();
+
+ @Test
+ void testSetAndPushDoNotCopyTheRepository() throws Exception {
+ getMockEndpoint("mock:result").expectedBodiesReceived("Hello World");
+
+ template.sendBody("direct:start", "Hello World");
+
+ assertMockEndpointsSatisfied();
+ assertEquals(0, safeCopiesBeforeMock.get(), "Set and Push should not
copy the claim check repository");
+ assertSame(repository, repositoryAtEnd.get(), "The exchange should
keep its claim check repository");
+ assertTrue(repository.contains("a") && repository.contains("b"), "The
repository should keep the claim checks");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start")
+ .process(e ->
e.setProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY, repository))
+ .claimCheck(ClaimCheckOperation.Set, "a")
+ .claimCheck(ClaimCheckOperation.Set, "b")
+ .claimCheck(ClaimCheckOperation.Push)
+ .claimCheck(ClaimCheckOperation.Pop)
+ .process(e -> {
+ // capture before the mock endpoint, which copies
the exchange
+
safeCopiesBeforeMock.set(repository.safeCopies.get());
+
repositoryAtEnd.set(e.getProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY));
+ })
+ .to("mock:result");
+ }
+ };
+ }
+
+ private static final class CountingClaimCheckRepository extends
DefaultClaimCheckRepository {
+
+ private final AtomicInteger safeCopies = new AtomicInteger();
+
+ @Override
+ public DefaultClaimCheckRepository safeCopy() {
+ safeCopies.incrementAndGet();
+ return super.safeCopy();
+ }
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/PooledExchangeClaimCheckEipSplitParallelTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/PooledExchangeClaimCheckEipSplitParallelTest.java
new file mode 100644
index 000000000000..78585a18372d
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/PooledExchangeClaimCheckEipSplitParallelTest.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.processor;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.impl.engine.PooledExchangeFactory;
+import org.apache.camel.impl.engine.PooledProcessorExchangeFactory;
+
+/**
+ * The same as {@link ClaimCheckEipSplitParallelTest} with pooled exchanges,
where the sub-exchanges are created by
+ * {@link PooledProcessorExchangeFactory}.
+ */
+class PooledExchangeClaimCheckEipSplitParallelTest extends
ClaimCheckEipSplitParallelTest {
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext camelContext = super.createCamelContext();
+ ExtendedCamelContext ecc = camelContext.getCamelContextExtension();
+ ecc.setExchangeFactory(new PooledExchangeFactory());
+ ecc.setProcessorExchangeFactory(new PooledProcessorExchangeFactory());
+ return camelContext;
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/util/ExchangeHelperTest.java
b/core/camel-core/src/test/java/org/apache/camel/util/ExchangeHelperTest.java
index c2c03180efd1..14de107bf3b9 100644
---
a/core/camel-core/src/test/java/org/apache/camel/util/ExchangeHelperTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/util/ExchangeHelperTest.java
@@ -23,12 +23,15 @@ import java.util.Map;
import org.apache.camel.ContextTestSupport;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
+import org.apache.camel.ExchangePropertyKey;
import org.apache.camel.Message;
import org.apache.camel.NoSuchBeanException;
import org.apache.camel.NoSuchHeaderException;
import org.apache.camel.NoSuchPropertyException;
import org.apache.camel.converter.stream.InputStreamCache;
import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.processor.DefaultClaimCheckRepository;
+import org.apache.camel.spi.ClaimCheckRepository;
import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.DefaultMessage;
import org.apache.camel.support.ExchangeHelper;
@@ -299,6 +302,25 @@ public class ExchangeHelperTest extends ContextTestSupport
{
assertNull(in.getExchange());
}
+ @Test
+ public void
testCopyExchangeWithPropertiesDoesNotShareClaimCheckRepository() {
+ DefaultClaimCheckRepository repo = new DefaultClaimCheckRepository();
+ Exchange stored = new DefaultExchange(context);
+ repo.add("original", stored);
+ exchange.setProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY, repo);
+
+ Exchange copy = ExchangeHelper.copyExchangeWithProperties(exchange,
context);
+
+ ClaimCheckRepository copyRepo
+ = copy.getProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY,
ClaimCheckRepository.class);
+ assertNotSame(repo, copyRepo, "The copy should get its own claim check
repository");
+ assertSame(stored, copyRepo.get("original"), "The copy should get the
claim checks of the exchange");
+
+ copyRepo.add("part", new DefaultExchange(context));
+ assertFalse(repo.contains("part"), "A claim check stored by the copy
should not be visible to the exchange");
+ assertSame(repo,
exchange.getProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY));
+ }
+
@Override
@BeforeEach
public void setUp() throws Exception {
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
b/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
index fdaa2c467fe6..102bee085166 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
@@ -175,6 +175,9 @@ abstract class AbstractExchange implements Exchange,
ExchangeExtension {
exchange.internalProperties.computeIfPresent(ExchangePropertyKey.MESSAGE_HISTORY,
(k, v) -> new
CopyOnWriteArrayList<>((List<MessageHistory>) v));
}
+ // the claim check repository is scoped per exchange, so the copy must
not share it
+
exchange.internalProperties.computeIfPresent(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY,
+ (k, v) -> v instanceof SafeCopyProperty scp ? scp.safeCopy() :
v);
return exchange;
}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
index d7ecd3df23f8..773a7335e4f6 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
@@ -49,6 +49,7 @@ import org.apache.camel.NoSuchPropertyException;
import org.apache.camel.NoTypeConversionAvailableException;
import org.apache.camel.Route;
import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.SafeCopyProperty;
import org.apache.camel.StreamCache;
import org.apache.camel.TypeConversionException;
import org.apache.camel.VariableAware;
@@ -844,6 +845,14 @@ public final class ExchangeHelper {
}
}
+ private static void setClaimCheckRepository(Exchange target, Exchange
source) {
+ // the claim check repository is scoped per exchange, so the copy must
not share it (as in Exchange.copy())
+ final Object repo =
source.getProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY);
+ if (repo instanceof SafeCopyProperty scp) {
+ target.setProperty(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY,
scp.safeCopy());
+ }
+ }
+
/**
* Copies the exchange but the copy will be tied to the given context
*
@@ -854,6 +863,7 @@ public final class ExchangeHelper {
Exchange answer =
exchange.getExchangeExtension().createCopyWithProperties(context);
setMessageHistory(answer, exchange);
+ setClaimCheckRepository(answer, exchange);
answer.setIn(exchange.getIn().copy());
if (exchange.hasOut()) {
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index b3546fb8648a..e16f9a2740bc 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -847,6 +847,20 @@ unknown property of a bean created through its builder
names the properties the
`PropertyBindingSupport.setPropertiesOnTarget` now reports an unknown property
with the property name and value, as
it rethrew the `PropertyBindingException` with its null cause since 4.21,
which surfaced as `NullPointerException: e`.
+=== camel-core - Claim Check EIP - exchange copies get their own claim check
repository
+
+The Claim Check EIP keeps its claim checks in a repository that belongs to the
exchange. The javadoc of
+`ClaimCheckProcessor` already says that the repository is "not shared among
Exchanges", but a copy of an exchange
+used the same repository instance as the exchange it was copied from. This
only happened when the exchange had used
+the Claim Check before the copy was made, because the repository is created on
first use. The exchanges created by
+the Splitter, Multicast, Recipient List and Wire Tap EIPs, and the exchanges
sent to a SEDA or Disruptor queue, then shared the
+repository of the parent, so with parallel processing a Get or Pop could
return the message of another part.
+
+Such a copy now gets its own copy of the repository. It starts with the claim
checks of the parent, so a part can
+still get what the parent stored, but what a part stores, removes, pushes or
pops 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 (the exchange that the aggregation strategy returned), as it already
did for the other exchange properties.
+
=== camel-core - Splitter unwraps Exchange parts
The Splitter EIP now unwraps parts of type `org.apache.camel.Exchange`, the
same way it already unwrapped parts of