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

davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.22.x by this push:
     new 6d6739dc2572 [backport camel-4.22.x] CAMEL-25559: camel-core - 
PooledExchangeFactory puts an exchange that was given back without being routed 
into the pool twice (#27741)
6d6739dc2572 is described below

commit 6d6739dc2572700d950abf02bcee5ca528727abe
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue Oct 13 07:58:29 2026 +0200

    [backport camel-4.22.x] CAMEL-25559: camel-core - PooledExchangeFactory 
puts an exchange that was given back without being routed into the pool twice 
(#27741)
    
    Backport of #27698. With the pooled exchange factory, a consumer that gave 
back
    an exchange it did not route put it into the pool twice, so the next two
    messages shared one exchange object. PooledExchangeFactory.release now 
offers an
    auto-release exchange to the pool exactly once.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/impl/engine/PooledExchangeFactory.java   |  55 +++++-
 ...tEagerMaxMessagesPerPollPooledExchangeTest.java |  69 ++++++++
 ...nsumeReadLockNotAcquiredPooledExchangeTest.java | 146 ++++++++++++++++
 .../PooledExchangeFactoryReleaseNotRoutedTest.java | 188 +++++++++++++++++++++
 4 files changed, 451 insertions(+), 7 deletions(-)

diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/PooledExchangeFactory.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/PooledExchangeFactory.java
index 24392c5f280b..c7861ddb1613 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/PooledExchangeFactory.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/PooledExchangeFactory.java
@@ -93,13 +93,49 @@ public final class PooledExchangeFactory extends 
PrototypeExchangeFactory {
         return exchange;
     }
 
+    /**
+     * Releases the exchange back into the pool.
+     * <p/>
+     * An exchange created with auto release is put into the pool when it is 
done. If it is not done yet (it was given
+     * back without being routed) this method does that and returns whether 
the pool took it, so false when the pool was
+     * full and the exchange was discarded. If it is already done, its unit of 
work has handed it to the pool before,
+     * and this method does nothing and returns true; whether the pool took it 
at that time is not known here.
+     */
     @Override
     public boolean release(Exchange exchange) {
         try {
             // done exchange before returning to pool
             PooledExchange ee = (PooledExchange) exchange;
-            ee.done();
 
+            if (ee.isAutoRelease()) {
+                // an auto-release exchange is offered to the pool once: by 
its on-done task when the unit of work is
+                // done, or here when the exchange was given back without 
being routed. Offering it in both places
+                // would put it in the pool twice.
+                if (ee.getClock().getCreated() <= 0) {
+                    // already done (the clock is unset by done), so the 
on-done task has offered it
+                    return true;
+                }
+                // done() would run the on-done task and offer the exchange, 
but the result of that offer cannot be
+                // read from here. The caller owns the exchange until it is 
offered, so take the task off for this
+                // call, offer the exchange here, and put the task back before 
the offer makes the exchange visible
+                // to other threads.
+                ee.onDone(null);
+                try {
+                    ee.done();
+                } finally {
+                    ee.onDone(onDone);
+                }
+            } else {
+                ee.done();
+            }
+        } catch (Exception e) {
+            return discard(exchange);
+        }
+        return doRelease(exchange);
+    }
+
+    private boolean doRelease(Exchange exchange) {
+        try {
             // only release back in pool if reset was success
             boolean inserted = pool.offer(exchange);
 
@@ -112,12 +148,16 @@ public final class PooledExchangeFactory extends 
PrototypeExchangeFactory {
             }
             return inserted;
         } catch (Exception e) {
-            if (statisticsEnabled) {
-                statistics.discarded.increment();
-            }
-            LOG.debug("Error resetting exchange: {}. This exchange is 
discarded.", exchange);
-            return false;
+            return discard(exchange);
+        }
+    }
+
+    private boolean discard(Exchange exchange) {
+        if (statisticsEnabled) {
+            statistics.discarded.increment();
         }
+        LOG.debug("Error resetting exchange: {}. This exchange is discarded.", 
exchange);
+        return false;
     }
 
     private PooledExchange createPooledExchange(Endpoint fromEndpoint, boolean 
autoRelease) {
@@ -157,7 +197,8 @@ public final class PooledExchangeFactory extends 
PrototypeExchangeFactory {
 
         @Override
         public void onDone(Exchange exchange) {
-            release(exchange);
+            // the exchange is done (reset) when this is called
+            doRelease(exchange);
         }
     }
 
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumeNotEagerMaxMessagesPerPollPooledExchangeTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumeNotEagerMaxMessagesPerPollPooledExchangeTest.java
new file mode 100644
index 000000000000..ae3a4abc31a4
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumeNotEagerMaxMessagesPerPollPooledExchangeTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.component.file;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.impl.engine.PooledExchangeFactory;
+import org.junit.jupiter.api.Test;
+
+/**
+ * With eagerMaxMessagesPerPoll=false the consumer creates an exchange for 
every file of the poll and gives back the
+ * ones beyond maxMessagesPerPoll without routing them. With pooled exchanges, 
the files of the next poll must each get
+ * their own exchange.
+ */
+class FileConsumeNotEagerMaxMessagesPerPollPooledExchangeTest extends 
ContextTestSupport {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext camelContext = super.createCamelContext();
+        camelContext.getCamelContextExtension().setExchangeFactory(new 
PooledExchangeFactory());
+        return camelContext;
+    }
+
+    @Test
+    void filesBeyondMaxMessagesPerPollAreConsumedByTheNextPoll() throws 
Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedBodiesReceived("AAA", "BBB", "CCC", "DDD");
+        mock.expectedHeaderValuesReceivedInAnyOrder(Exchange.FILE_NAME_ONLY, 
"a.txt", "b.txt", "c.txt", "d.txt");
+
+        template.sendBodyAndHeader(fileUri(), "AAA", Exchange.FILE_NAME, 
"a.txt");
+        template.sendBodyAndHeader(fileUri(), "BBB", Exchange.FILE_NAME, 
"b.txt");
+        template.sendBodyAndHeader(fileUri(), "CCC", Exchange.FILE_NAME, 
"c.txt");
+        template.sendBodyAndHeader(fileUri(), "DDD", Exchange.FILE_NAME, 
"d.txt");
+
+        context.getRouteController().startRoute("foo");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from(fileUri("?initialDelay=0&delay=10&maxMessagesPerPoll=2&eagerMaxMessagesPerPoll=false&sortBy=file:name"))
+                        .routeId("foo").autoStartup(false)
+                        .convertBodyTo(String.class)
+                        .to("mock:result");
+            }
+        };
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumeReadLockNotAcquiredPooledExchangeTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumeReadLockNotAcquiredPooledExchangeTest.java
new file mode 100644
index 000000000000..e42619361377
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumeReadLockNotAcquiredPooledExchangeTest.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.component.file;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.LoggingLevel;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.impl.engine.PooledExchangeFactory;
+import org.apache.camel.spi.Registry;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * The consumer gives back the exchange of every file whose read lock it could 
not get, without routing it. With pooled
+ * exchanges, the files of the next poll must each get their own exchange.
+ */
+class FileConsumeReadLockNotAcquiredPooledExchangeTest extends 
ContextTestSupport {
+
+    private final RefuseTwiceReadLockStrategy readLockStrategy = new 
RefuseTwiceReadLockStrategy();
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext camelContext = super.createCamelContext();
+        camelContext.getCamelContextExtension().setExchangeFactory(new 
PooledExchangeFactory());
+        return camelContext;
+    }
+
+    @Override
+    protected Registry createCamelRegistry() throws Exception {
+        Registry registry = super.createCamelRegistry();
+        registry.bind("refuseTwice", readLockStrategy);
+        return registry;
+    }
+
+    @Test
+    void filesWithoutReadLockAreConsumedByTheNextPoll() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedBodiesReceived("AAA", "BBB");
+        mock.expectedHeaderValuesReceivedInAnyOrder(Exchange.FILE_NAME_ONLY, 
"a.txt", "b.txt");
+
+        template.sendBodyAndHeader(fileUri(), "AAA", Exchange.FILE_NAME, 
"a.txt");
+        template.sendBodyAndHeader(fileUri(), "BBB", Exchange.FILE_NAME, 
"b.txt");
+
+        context.getRouteController().startRoute("foo");
+
+        assertMockEndpointsSatisfied();
+        // both files were refused in the first poll and locked in the second 
one
+        assertEquals(4, readLockStrategy.attempts.get());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from(fileUri("?initialDelay=0&delay=10&sortBy=file:name&exclusiveReadLockStrategy=#refuseTwice"))
+                        .routeId("foo").autoStartup(false)
+                        .convertBodyTo(String.class)
+                        .to("mock:result");
+            }
+        };
+    }
+
+    /**
+     * Does not give the lock for the first two files it is asked for (the two 
files of the first poll).
+     */
+    private static final class RefuseTwiceReadLockStrategy implements 
GenericFileExclusiveReadLockStrategy<File> {
+
+        private final AtomicInteger attempts = new AtomicInteger();
+
+        @Override
+        public void prepareOnStartup(GenericFileOperations<File> operations, 
GenericFileEndpoint<File> endpoint) {
+            // noop
+        }
+
+        @Override
+        public boolean acquireExclusiveReadLock(
+                GenericFileOperations<File> operations, GenericFile<File> 
file, Exchange exchange) {
+            return attempts.incrementAndGet() > 2;
+        }
+
+        @Override
+        public void releaseExclusiveReadLockOnAbort(
+                GenericFileOperations<File> operations, GenericFile<File> 
file, Exchange exchange) {
+            // noop
+        }
+
+        @Override
+        public void releaseExclusiveReadLockOnRollback(
+                GenericFileOperations<File> operations, GenericFile<File> 
file, Exchange exchange) {
+            // noop
+        }
+
+        @Override
+        public void releaseExclusiveReadLockOnCommit(
+                GenericFileOperations<File> operations, GenericFile<File> 
file, Exchange exchange) {
+            // noop
+        }
+
+        @Override
+        public void setTimeout(long timeout) {
+            // noop
+        }
+
+        @Override
+        public void setCheckInterval(long checkInterval) {
+            // noop
+        }
+
+        @Override
+        public void setReadLockLoggingLevel(LoggingLevel readLockLoggingLevel) 
{
+            // noop
+        }
+
+        @Override
+        public void setMarkerFiler(boolean markerFile) {
+            // noop
+        }
+
+        @Override
+        public void setDeleteOrphanLockFiles(boolean deleteOrphanLockFiles) {
+            // noop
+        }
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/PooledExchangeFactoryReleaseNotRoutedTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/PooledExchangeFactoryReleaseNotRoutedTest.java
new file mode 100644
index 000000000000..5cb0d8ea6b51
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/PooledExchangeFactoryReleaseNotRoutedTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.impl.engine;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.PooledExchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.ExchangeFactoryManager;
+import org.apache.camel.spi.PooledObjectFactory;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A consumer that gives back an exchange it never routed (a batch cut down to 
maxMessagesPerPoll, a consumer that is
+ * stopping, a failure while the message is prepared) must put it into the 
pool once, so that the next two exchanges are
+ * two different objects.
+ */
+class PooledExchangeFactoryReleaseNotRoutedTest extends ContextTestSupport {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext camelContext = super.createCamelContext();
+        ExtendedCamelContext ecc = camelContext.getCamelContextExtension();
+        ecc.setExchangeFactory(new PooledExchangeFactory());
+        ecc.getExchangeFactory().setStatisticsEnabled(true);
+        return camelContext;
+    }
+
+    @Test
+    void autoReleaseExchangeThatWasNotRoutedIsPooledOnce() {
+        Consumer consumer = context.getRoute("foo").getConsumer();
+
+        Exchange exchange = consumer.createExchange(true);
+        exchange.getMessage().setBody("not routed");
+        consumer.releaseExchange(exchange, true);
+
+        assertPooledOnce(consumer, exchange);
+    }
+
+    @Test
+    void 
autoReleaseExchangeThatWasNotRoutedIsPooledOnceWhenReleasedAsNotAutoRelease() {
+        Consumer consumer = context.getRoute("foo").getConsumer();
+
+        // several consumers create their exchanges with auto release and give 
back the ones they do not route with false
+        Exchange exchange = consumer.createExchange(true);
+        exchange.getMessage().setBody("not routed");
+        consumer.releaseExchange(exchange, false);
+
+        assertPooledOnce(consumer, exchange);
+    }
+
+    /**
+     * Releasing an exchange a second time is still a bug of the consumer. It 
does no harm here only because nobody took
+     * the exchange from the pool in between; otherwise the second release 
would reset the exchange of another message.
+     */
+    @Test
+    void autoReleaseExchangeThatWasRoutedIsPooledOnceWhenReleasedAgain() 
throws Exception {
+        Consumer consumer = context.getRoute("foo").getConsumer();
+
+        Exchange exchange = consumer.createExchange(true);
+        exchange.getMessage().setBody("routed");
+        // the unit of work of the route is done here and gives the exchange 
back to the pool
+        consumer.getProcessor().process(exchange);
+        assertEquals(1, statistics().getReleasedCounter());
+
+        consumer.releaseExchange(exchange, true);
+
+        assertPooledOnce(consumer, exchange);
+    }
+
+    @Test
+    void exchangeWithoutAutoReleaseIsPooledOnce() {
+        Consumer consumer = context.getRoute("foo").getConsumer();
+
+        Exchange exchange = consumer.createExchange(false);
+        exchange.getMessage().setBody("not routed");
+        consumer.releaseExchange(exchange, false);
+
+        assertPooledOnce(consumer, exchange);
+    }
+
+    @Test
+    void 
releaseReturnsFalseWhenThePoolIsFullAndTheNotRoutedExchangeIsDiscarded() {
+        PooledExchangeFactory factory = newFactoryWithCapacityOne();
+
+        Exchange first = factory.create(true);
+        Exchange second = factory.create(true);
+
+        assertTrue(factory.release(first), "the pool has room for the first 
exchange");
+        assertFalse(factory.release(second), "the pool is full, so the second 
exchange is discarded");
+
+        assertEquals(1, factory.getSize(), "exchanges in the pool");
+        assertEquals(1, factory.getStatistics().getReleasedCounter());
+        assertEquals(1, factory.getStatistics().getDiscardedCounter());
+
+        // the discarded exchange was reset
+        assertEquals(0, second.getClock().getCreated());
+        assertSame(first, factory.create(true));
+        assertNotSame(first, factory.create(true));
+    }
+
+    @Test
+    void releaseReturnsTrueWhenTheExchangeWasAlreadyGivenBackByItsUnitOfWork() 
{
+        PooledExchangeFactory factory = newFactoryWithCapacityOne();
+
+        Exchange routed = factory.create(true);
+        // what the unit of work does when the route is done
+        ((PooledExchange) routed).done();
+        assertEquals(1, factory.getSize(), "exchanges in the pool");
+
+        assertTrue(factory.release(routed), "nothing to do: the exchange is in 
the pool already");
+
+        assertEquals(1, factory.getSize(), "exchanges in the pool");
+        assertEquals(1, factory.getStatistics().getReleasedCounter());
+        assertEquals(0, factory.getStatistics().getDiscardedCounter());
+    }
+
+    private PooledExchangeFactory newFactoryWithCapacityOne() {
+        PooledExchangeFactory factory = new PooledExchangeFactory();
+        factory.setCamelContext(context);
+        factory.setCapacity(1);
+        factory.setStatisticsEnabled(true);
+        // build creates the pool; the factory is not started, as it has no 
consumer to be registered with
+        factory.build();
+        return factory;
+    }
+
+    private void assertPooledOnce(Consumer consumer, Exchange released) {
+        // the exchange was reset
+        assertNull(released.getMessage().getBody());
+
+        assertEquals(1, manager().getPooledCounter(), "exchanges in the pool");
+        assertEquals(1, statistics().getCreatedCounter());
+        assertEquals(1, statistics().getReleasedCounter());
+        assertEquals(0, statistics().getDiscardedCounter());
+
+        Exchange first = consumer.createExchange(true);
+        Exchange second = consumer.createExchange(true);
+
+        assertSame(released, first, "the pooled exchange is used again");
+        assertNotSame(first, second, "two messages must not share one 
exchange");
+        assertEquals(0, manager().getPooledCounter(), "exchanges in the pool");
+        assertEquals(2, statistics().getCreatedCounter());
+        assertEquals(1, statistics().getAcquiredCounter());
+    }
+
+    private ExchangeFactoryManager manager() {
+        return context.getCamelContextExtension().getExchangeFactoryManager();
+    }
+
+    private PooledObjectFactory.Statistics statistics() {
+        return manager().getStatistics();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").routeId("foo").to("mock:result");
+            }
+        };
+    }
+}

Reply via email to