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 694245fae833 CAMEL-25850: camel-jetty - stopping one Jetty server 
stops only its own thread pool (#28323)
694245fae833 is described below

commit 694245fae83377cce6dc6d01315ff54f12f66f74
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Oct 23 22:51:45 2026 +0530

    CAMEL-25850: camel-jetty - stopping one Jetty server stops only its own 
thread pool (#28323)
    
    * CAMEL-25850: camel-jetty - stopping one Jetty server stops only its own 
thread pool
    
    With minThreads or maxThreads on the component, every Jetty server gets
    a thread pool of its own, but the component kept only the pool of the
    last server it created in one field, and stopped that pool when any
    server was no longer used. With servers on two ports, stopping the
    routes of the first port stopped the thread pool of the second server,
    which then no longer accepted connections while its routes stayed
    started; the pool of the first server was never stopped.
    
    The pool is now kept with the server it was created for, in the
    ConnectorRef, and stopped with that server.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    * CAMEL-25850: camel-jetty - remove the unused constructor of ConnectorRef
    
    Review of #28323 (nit): connect() is the only place that creates a 
ConnectorRef, and it passes
    the thread pool of the server, so the 3-argument constructor has no callers 
left.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../camel/component/jetty/JettyHttpComponent.java  | 46 ++++++++---
 .../jetty/JettyThreadPoolTwoServersTest.java       | 96 ++++++++++++++++++++++
 2 files changed, 129 insertions(+), 13 deletions(-)

diff --git 
a/components/camel-jetty-common/src/main/java/org/apache/camel/component/jetty/JettyHttpComponent.java
 
b/components/camel-jetty-common/src/main/java/org/apache/camel/component/jetty/JettyHttpComponent.java
index d99f7883407d..c51ea97b6d7e 100644
--- 
a/components/camel-jetty-common/src/main/java/org/apache/camel/component/jetty/JettyHttpComponent.java
+++ 
b/components/camel-jetty-common/src/main/java/org/apache/camel/component/jetty/JettyHttpComponent.java
@@ -123,7 +123,6 @@ public abstract class JettyHttpComponent extends 
HttpCommonComponent
     protected boolean useXForwardedForHeader;
     private Integer proxyPort;
     private boolean sendServerVersion = true;
-    private QueuedThreadPool defaultQueuedThreadPool;
     private String filesLocation;
     private Long maxFileSize = -1L;
     private Long maxRequestSize = -1L;
@@ -137,12 +136,15 @@ public abstract class JettyHttpComponent extends 
HttpCommonComponent
         final Server server;
         final ServerConnector connector;
         final CamelServlet servlet;
+        // the thread pool created for this server from minThreads / 
maxThreads, stopped with the server
+        final QueuedThreadPool threadPool;
         int refCount;
 
-        ConnectorRef(Server server, ServerConnector connector, CamelServlet 
servlet) {
+        ConnectorRef(Server server, ServerConnector connector, CamelServlet 
servlet, QueuedThreadPool threadPool) {
             this.server = server;
             this.connector = connector;
             this.servlet = servlet;
+            this.threadPool = threadPool;
             increment();
         }
 
@@ -359,7 +361,8 @@ public abstract class JettyHttpComponent extends 
HttpCommonComponent
 
             connectorRef = new ConnectorRef(
                     server, connector,
-                    createServletForConnector(server, connector, 
endpoint.getHandlers(), endpoint));
+                    createServletForConnector(server, connector, 
endpoint.getHandlers(), endpoint),
+                    ownThreadPool(server));
             // must enable session before we start
             if (endpoint.isSessionSupport()) {
                 enableSessionSupport(connectorRef.server, connectorKey);
@@ -554,15 +557,7 @@ public abstract class JettyHttpComponent extends 
HttpCommonComponent
                 this.removeServerMBean(connectorRef.server);
                 //mbContainer.removeBean(connectorRef.connector);
             }
-            if (defaultQueuedThreadPool != null) {
-                try {
-                    defaultQueuedThreadPool.stop();
-                } catch (Exception t) {
-                    defaultQueuedThreadPool.destroy();
-                } finally {
-                    defaultQueuedThreadPool = null;
-                }
-            }
+            stopThreadPool(connectorRef);
             return null;
         }
         return connectorRef;
@@ -1332,10 +1327,34 @@ public abstract class JettyHttpComponent extends 
HttpCommonComponent
         }
     }
 
+    /**
+     * The thread pool that {@link #createServer()} created for the server 
from minThreads / maxThreads, which the
+     * server does not stop: each server has its own, so it is kept with the 
server and stopped when the server is no
+     * longer used, not when another server is.
+     */
+    private QueuedThreadPool ownThreadPool(Server server) {
+        if (threadPool == null && (minThreads != null || maxThreads != null)
+                && server.getThreadPool() instanceof QueuedThreadPool 
queuedThreadPool) {
+            return queuedThreadPool;
+        }
+        return null;
+    }
+
+    private static void stopThreadPool(ConnectorRef connectorRef) {
+        QueuedThreadPool pool = connectorRef.threadPool;
+        if (pool != null) {
+            try {
+                pool.stop();
+            } catch (Exception t) {
+                pool.destroy();
+            }
+        }
+    }
+
     protected Server createServer() {
 
         ThreadPool tp = threadPool;
-        defaultQueuedThreadPool = null;
+        QueuedThreadPool defaultQueuedThreadPool = null;
         // configure thread pool if min/max given
         if (minThreads != null || maxThreads != null) {
             if (getThreadPool() != null) {
@@ -1430,6 +1449,7 @@ public abstract class JettyHttpComponent extends 
HttpCommonComponent
                 // registered MBeans when Camel is done with the managed 
objects.
                 removeServerMBean(connectorRef.server);
                 connectorRef.server.stop();
+                stopThreadPool(connectorRef);
                 //removeServerMBean(connectorRef.connector);
                 CONNECTORS.remove(connectorEntry.getKey());
             }
diff --git 
a/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/JettyThreadPoolTwoServersTest.java
 
b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/JettyThreadPoolTwoServersTest.java
new file mode 100644
index 000000000000..1a744179e6c0
--- /dev/null
+++ 
b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/JettyThreadPoolTwoServersTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.jetty;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * With minThreads / maxThreads on the component, each Jetty server has its 
own thread pool: stopping the route of one
+ * server does not stop the thread pool of another one, and each thread pool 
is stopped with its own server.
+ */
+class JettyThreadPoolTwoServersTest extends BaseJettyTest {
+
+    private final HttpClient client = 
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
+
+    @Test
+    void stoppingOneServerKeepsTheOtherServing() throws Exception {
+        assertEquals("a", get(getPort(), "a"));
+        assertEquals("b", get(getPort2(), "b"));
+
+        context.getRouteController().stopRoute("a");
+
+        assertEquals("b", get(getPort2(), "b"), "the server of route b no 
longer answers after route a stopped");
+    }
+
+    @Test
+    void stoppingBothServersStopsBothThreadPools() throws Exception {
+        assertEquals("a", get(getPort(), "a"));
+        assertEquals("b", get(getPort2(), "b"));
+        // the thread pools of the two servers of this test (other test 
classes may run Jetty servers at the same time)
+        List<String> pools = List.of(threadPoolName(getPort()), 
threadPoolName(getPort2()));
+
+        context.getRouteController().stopRoute("b");
+        context.getRouteController().stopRoute("a");
+
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+                () -> assertEquals(0, countThreads(pools), "threads of a Jetty 
server thread pool left running"));
+    }
+
+    private static String threadPoolName(int port) {
+        return ((QueuedThreadPool) 
JettyHttpComponent.CONNECTORS.get("http:localhost:" + 
port).server.getThreadPool())
+                .getName();
+    }
+
+    private static long countThreads(List<String> pools) {
+        return Thread.getAllStackTraces().keySet().stream()
+                .filter(t -> t.isAlive() && pools.stream().anyMatch(pool -> 
t.getName().startsWith(pool))).count();
+    }
+
+    private String get(int port, String path) throws Exception {
+        HttpRequest request = 
HttpRequest.newBuilder(URI.create("http://localhost:"; + port + "/" + path))
+                .timeout(Duration.ofSeconds(5)).build();
+        return client.send(request, 
HttpResponse.BodyHandlers.ofString()).body();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                JettyHttpComponent jetty = (JettyHttpComponent) 
context.getComponent("jetty");
+                jetty.setMinThreads(4);
+                jetty.setMaxThreads(10);
+
+                
from("jetty://http://localhost:{{port}}/a";).routeId("a").setBody(constant("a"));
+                
from("jetty://http://localhost:{{port2}}/b";).routeId("b").setBody(constant("b"));
+            }
+        };
+    }
+}

Reply via email to