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"));
+ }
+ };
+ }
+}