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 7251b7099649 CAMEL-25066: camel-core - Do not stop an endpoint on 
producer cache eviction while another producer cache uses it (#27523)
7251b7099649 is described below

commit 7251b7099649fdcb26a20388e5775cdd492c185f
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Oct 8 19:13:14 2026 +0530

    CAMEL-25066: camel-core - Do not stop an endpoint on producer cache 
eviction while another producer cache uses it (#27523)
    
    Endpoints are shared per CamelContext (toD, recipientList, routingSlip and
    ProducerTemplate resolve the same Endpoint instance), but each has its own
    producer cache. When one cache evicted the producer of a dynamic endpoint,
    ServicePool stopped the endpoint and removed it from the registry even while
    another cache still held a started producer of it, breaking endpoints that
    release resources in doStop (e.g. HTTP closes its client).
    
    Pooled singleton producers and polling consumers are now registered as
    CamelContext services, and isEndpointInUse() also treats the endpoint as in
    use while another EndpointAware service of it is registered. cleanupEvicts
    stops the evicted pool first so its own producer does not count. A dynamic
    endpoint nothing else uses is still stopped and removed as before.
    
    Closes #27523
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../impl/ProducerCacheEvictSharedEndpointTest.java | 165 +++++++++++++++++++++
 .../apache/camel/support/cache/ServicePool.java    |  17 ++-
 2 files changed, 177 insertions(+), 5 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/ProducerCacheEvictSharedEndpointTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/ProducerCacheEvictSharedEndpointTest.java
new file mode 100644
index 000000000000..cbbc350cb62e
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/ProducerCacheEvictSharedEndpointTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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;
+
+import java.util.Map;
+
+import org.apache.camel.AsyncProducer;
+import org.apache.camel.Consumer;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.support.cache.DefaultProducerCache;
+import org.apache.camel.support.service.ServiceHelper;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Endpoints are shared by the producer caches (resolved from the endpoint 
registry), so evicting the producer of an
+ * endpoint from one producer cache must not stop the endpoint while another 
producer cache still uses it.
+ */
+class ProducerCacheEvictSharedEndpointTest extends ContextTestSupport {
+
+    @Test
+    void testEvictDoesNotStopEndpointOfOtherRoute() {
+        // route b keeps its producer of seda:shared in its producer cache
+        template.sendBodyAndHeader("direct:b", "B", "uri", "seda:shared");
+        Endpoint shared = context.hasEndpoint("seda:shared");
+
+        // route a caches one producer, so this evicts its producer of 
seda:shared (cleaned up on release)
+        template.sendBodyAndHeader("direct:a", "A", "uri", "seda:shared");
+        template.sendBodyAndHeader("direct:a", "A", "uri", "seda:other");
+
+        assertTrue(ServiceHelper.isStarted(shared), "seda:shared is still used 
by route b and must not be stopped");
+        assertSame(shared, context.hasEndpoint("seda:shared"), "seda:shared 
should still be registered");
+    }
+
+    @Test
+    void testEvictDoesNotStopEndpointOfProducerTemplate() throws Exception {
+        context.addComponent("closing", new ClosingComponent());
+        Endpoint shared = context.getEndpoint("closing:shared");
+        ProducerTemplate other = context.createProducerTemplate();
+        try {
+            other.sendBody(shared, "Hello");
+
+            // route a caches one producer, so this evicts its producer of 
closing:shared (cleaned up on release)
+            template.sendBodyAndHeader("direct:a", "A", "uri", 
"closing:shared");
+            template.sendBodyAndHeader("direct:a", "A", "uri", 
"closing:other");
+
+            // fails if the endpoint was stopped (as with an endpoint that 
closes its client when stopped)
+            Exchange out = other.send(shared, e -> 
e.getMessage().setBody("World"));
+            assertNull(out.getException(), "The template should still be able 
to send to closing:shared");
+            assertTrue(ServiceHelper.isStarted(shared), "closing:shared is 
still used by the template and must not be stopped");
+        } finally {
+            other.stop();
+        }
+    }
+
+    @Test
+    void testEvictStopsEndpointNotInUseWhenPoolIsNotStoppedOnEviction() throws 
Exception {
+        context.addComponent("closing", new ClosingComponent());
+        context.addComponent("multi", new MultipleProducersComponent());
+        DefaultProducerCache cache = new DefaultProducerCache(this, context, 
2);
+        cache.start();
+        try {
+            Endpoint dynamic = context.getEndpoint("closing:dynamic");
+            cache.releaseProducer(dynamic, cache.acquireProducer(dynamic));
+
+            // two producers of a non-singleton endpoint (one pool) evict the 
producer of closing:dynamic, and as the
+            // cache still has no more pools than its capacity, the evicted 
pool is only stopped by the cleanup
+            Endpoint multi = context.getEndpoint("multi:foo");
+            AsyncProducer first = cache.acquireProducer(multi);
+            AsyncProducer second = cache.acquireProducer(multi);
+            cache.releaseProducer(multi, first);
+            cache.releaseProducer(multi, second);
+
+            // the cleanup of the evicted singleton pool runs when a singleton 
producer is acquired
+            Endpoint other = context.getEndpoint("closing:other");
+            cache.releaseProducer(other, cache.acquireProducer(other));
+
+            assertFalse(ServiceHelper.isStarted(dynamic),
+                    "the dynamic endpoint not in use should be stopped to free 
resources");
+            assertNull(context.hasEndpoint("closing:dynamic"), "the dynamic 
endpoint not in use should be removed");
+        } finally {
+            cache.stop();
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:a").toD("${header.uri}", 1);
+                from("direct:b").toD("${header.uri}");
+            }
+        };
+    }
+
+    private static final class ClosingComponent extends DefaultComponent {
+        @Override
+        protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) {
+            return new ClosingEndpoint(uri, this);
+        }
+    }
+
+    private static final class MultipleProducersComponent extends 
DefaultComponent {
+        @Override
+        protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) {
+            return new ClosingEndpoint(uri, this) {
+                @Override
+                public boolean isSingletonProducer() {
+                    return false;
+                }
+            };
+        }
+    }
+
+    private static class ClosingEndpoint extends DefaultEndpoint {
+        ClosingEndpoint(String uri, DefaultComponent component) {
+            super(uri, component);
+        }
+
+        @Override
+        public Producer createProducer() {
+            return new DefaultProducer(this) {
+                @Override
+                public void process(Exchange exchange) {
+                    if (!ServiceHelper.isStarted(getEndpoint())) {
+                        throw new IllegalStateException("Endpoint is stopped: 
" + getEndpoint());
+                    }
+                }
+            };
+        }
+
+        @Override
+        public Consumer createConsumer(Processor processor) {
+            throw new UnsupportedOperationException();
+        }
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/cache/ServicePool.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/cache/ServicePool.java
index d6e61b9f60a6..c5c061c86fa8 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/cache/ServicePool.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/cache/ServicePool.java
@@ -29,6 +29,7 @@ import java.util.function.Function;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.Endpoint;
+import org.apache.camel.EndpointAware;
 import org.apache.camel.NonManagedService;
 import org.apache.camel.Route;
 import org.apache.camel.Service;
@@ -194,9 +195,14 @@ abstract class ServicePool<S extends Service> extends 
ServiceSupport implements
     }
 
     /**
-     * Whether the endpoint is (still) in use by the routes, and must 
therefore not be stopped when its producer is
-     * evicted: an endpoint that is static in the endpoint registry (resolved 
when the routes were setup), or that a
-     * route is consuming from.
+     * Whether the endpoint is (still) in use, and must therefore not be 
stopped when its producer is evicted: an
+     * endpoint that is static in the endpoint registry (resolved when the 
routes were setup), that a route is consuming
+     * from, or that another producer (or polling consumer) is still using, 
such as the producer cache of a toD in
+     * another route or of a ProducerTemplate, as the pooled services are 
added to CamelContext.
+     * <p>
+     * This is intentionally broad: any {@link EndpointAware} service on the 
context that uses the endpoint keeps it
+     * started, not only the producers and polling consumers pooled by a 
{@code ServicePool}, as stopping an endpoint
+     * that is still in use is worse than keeping one started.
      */
     private static boolean isEndpointInUse(Endpoint endpoint) {
         CamelContext context = endpoint.getCamelContext();
@@ -211,7 +217,7 @@ abstract class ServicePool<S extends Service> extends 
ServiceSupport implements
                 return true;
             }
         }
-        return false;
+        return context.hasService(s -> s instanceof EndpointAware ea && 
ea.getEndpoint() == endpoint) != null;
     }
 
     /**
@@ -300,11 +306,12 @@ abstract class ServicePool<S extends Service> extends 
ServiceSupport implements
                 for (Map.Entry<Endpoint, Pool<S>> entry : 
singlePoolEvicted.entrySet()) {
                     Endpoint e = entry.getKey();
                     Pool<S> p = entry.getValue();
+                    // stop the evicted service first, so it no longer counts 
as using the endpoint
+                    p.stop();
                     if (!isEndpointInUse(e)) {
                         // stop the endpoint as well (such as a dynamic 
endpoint from toD) to free its resources
                         doStop(e);
                     }
-                    p.stop();
                     singlePoolEvicted.remove(e);
                 }
             }

Reply via email to