This is an automated email from the ASF dual-hosted git repository.

Croway 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 9eaa29827812 CAMEL-24800: camel-core - pooled exchange copies must own 
their clock (#26553)
9eaa29827812 is described below

commit 9eaa29827812b851335b60f3c7d7c30de4da95d4
Author: Federico Mariani <[email protected]>
AuthorDate: Thu Sep 17 19:13:51 2026 +0200

    CAMEL-24800: camel-core - pooled exchange copies must own their clock 
(#26553)
    
    * CAMEL-24800: camel-core - pooled exchange copies must own their clock
    
    DefaultPooledExchange(Exchange parent) reused the parent's ResetableClock
    when the parent already had one. Splitter children are pooled copies of
    the Splitter's working copy, so parent and child shared one clock: when
    the working copy was released first, its done() unset the created time,
    and the child's done() then skipped the reset entirely. The dirty child
    went back into the pool, leaking exchange properties and variables into
    the next message's split children.
    
    Every pooled copy now gets its own ResetableClock.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    
    * CAMEL-24800: assert the first child carries the state before checking it 
does not leak
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Fable 5.1 <[email protected]>
---
 .../processor/PooledExchangeSplitLeakTest.java     | 78 ++++++++++++++++++++++
 .../camel/support/DefaultPooledExchange.java       | 10 +--
 2 files changed, 81 insertions(+), 7 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/PooledExchangeSplitLeakTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/PooledExchangeSplitLeakTest.java
new file mode 100644
index 000000000000..2e08a7d72ca1
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/PooledExchangeSplitLeakTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.impl.engine.PooledExchangeFactory;
+import org.apache.camel.impl.engine.PooledProcessorExchangeFactory;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class PooledExchangeSplitLeakTest extends ContextTestSupport {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext camelContext = super.createCamelContext();
+        ExtendedCamelContext ecc = camelContext.getCamelContextExtension();
+        ecc.setExchangeFactory(new PooledExchangeFactory());
+        ecc.setProcessorExchangeFactory(new PooledProcessorExchangeFactory());
+        return camelContext;
+    }
+
+    @Test
+    public void testSplitChildrenDoNotLeakStateBetweenMessages() throws 
Exception {
+        MockEndpoint mock = getMockEndpoint("mock:split");
+        mock.expectedMessageCount(2);
+
+        template.sendBody("direct:start", List.of("first"));
+        template.sendBody("direct:start", List.of("second"));
+
+        mock.assertIsSatisfied();
+
+        // the child of the first message carries the state set by the 
processor
+        assertEquals("from-first", 
mock.getExchanges().get(0).getProperty("leakProp"));
+        assertEquals("from-first", 
mock.getExchanges().get(0).getVariable("leak"));
+        // the child of the second message is a reused pooled exchange; it 
must not carry the first child's state
+        assertNull(mock.getExchanges().get(1).getProperty("leakProp"));
+        assertNull(mock.getExchanges().get(1).getVariable("leak"));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("direct:start").split(body())
+                        .process(e -> {
+                            if ("first".equals(e.getMessage().getBody())) {
+                                e.setProperty("leakProp", "from-first");
+                                e.setVariable("leak", "from-first");
+                            }
+                        })
+                        .to("mock:split");
+            }
+        };
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
index 9397fe32455f..89fb75996920 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
@@ -45,13 +45,9 @@ public final class DefaultPooledExchange extends 
AbstractExchange implements Poo
         super(parent);
         this.originalPattern = parent.getPattern();
 
-        Clock parentClock = parent.getClock();
-
-        if (parentClock instanceof ResetableClock rs) {
-            this.clock = rs;
-        } else {
-            this.clock = new ResetableClock(parent.getClock());
-        }
+        // the copy must own its clock: sharing the parent's clock makes 
done() on the parent unset the
+        // created time of the copy as well, and the copy is then returned to 
the pool without being reset
+        this.clock = new ResetableClock(parent.getClock());
     }
 
     public DefaultPooledExchange(CamelContext context, ExchangePattern 
pattern) {

Reply via email to