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 f6b9e1a5ba6b CAMEL-26554: camel-jetty - remove the connector of the
last consumer even when stopping it fails (#29114)
f6b9e1a5ba6b is described below
commit f6b9e1a5ba6b49477697ab4af2accc0cd7119cbb
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 4 20:00:32 2026 +0100
CAMEL-26554: camel-jetty - remove the connector of the last consumer even
when stopping it fails (#29114)
- CAMEL-26554: camel-jetty - remove the connector of the last consumer even
when stopping it fails
- CAMEL-26554: camel-jetty - a failure to stop the thread pool also goes
through stopStep
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../camel/component/jetty/JettyHttpComponent.java | 81 ++++++++++++---------
.../jetty/JettyConsumerStopFailedTest.java | 84 ++++++++++++++++++++++
2 files changed, 133 insertions(+), 32 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 c51ea97b6d7e..be3908471acb 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
@@ -66,6 +66,7 @@ import org.apache.camel.util.PropertiesHelper;
import org.apache.camel.util.StringHelper;
import org.apache.camel.util.URISupport;
import org.apache.camel.util.UnsafeUriCharactersEncoder;
+import org.apache.camel.util.function.ThrowingRunnable;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
@@ -532,35 +533,54 @@ public abstract class JettyHttpComponent extends
HttpCommonComponent
HttpCommonEndpoint endpoint = consumer.getEndpoint();
String connectorKey = getConnectorKey(endpoint);
- try {
- CONNECTORS.computeIfPresent(connectorKey, (cKey, connectorRef) -> {
- try {
- return disconnect(consumer, connectorRef);
- } catch (Exception e) {
- throw new RuntimeCamelException(e);
- }
- });
- } catch (RuntimeCamelException e) {
- throw (Exception) e.getCause();
+ // the failure to stop the connector of the last consumer, thrown once
the connector is removed
+ Exception[] stopFailure = new Exception[1];
+ CONNECTORS.computeIfPresent(connectorKey, (cKey, connectorRef) -> {
+ connectorRef.servlet.disconnect(consumer);
+ if (connectorRef.decrement() == 0) {
+ // remove the connector even when stopping it fails (for
example on an interrupted thread),
+ // or the next consumer on this port would reuse a connector
that no longer listens
+ stopFailure[0] = stopConnector(connectorRef);
+ return null;
+ }
+ return connectorRef;
+ });
+ if (stopFailure[0] != null) {
+ throw stopFailure[0];
}
}
- private ConnectorRef disconnect(HttpConsumer consumer, ConnectorRef
connectorRef) throws Exception {
- connectorRef.servlet.disconnect(consumer);
- if (connectorRef.decrement() == 0) {
- connectorRef.server.removeConnector(connectorRef.connector);
- connectorRef.connector.stop();
- connectorRef.server.stop();
- // Camel controls the lifecycle of these entities so remove the
- // registered MBeans when Camel is done with the managed objects.
- if (mbContainer != null) {
- this.removeServerMBean(connectorRef.server);
- //mbContainer.removeBean(connectorRef.connector);
+ /**
+ * Stops the connector, the server and the thread pool of a connector that
no consumer uses anymore. Each one is
+ * stopped even when stopping another one fails.
+ *
+ * @return the first failure (with the later ones as suppressed), or null
when all stopped
+ */
+ private Exception stopConnector(ConnectorRef connectorRef) {
+ Exception failure = null;
+ failure = stopStep(failure, () ->
connectorRef.server.removeConnector(connectorRef.connector));
+ failure = stopStep(failure, connectorRef.connector::stop);
+ failure = stopStep(failure, connectorRef.server::stop);
+ // Camel controls the lifecycle of these entities so remove the
+ // registered MBeans when Camel is done with the managed objects
+ // (once the server is stopped, as disconnect has always done)
+ if (mbContainer != null) {
+ failure = stopStep(failure, () ->
removeServerMBean(connectorRef.server));
+ }
+ failure = stopStep(failure, () -> stopThreadPool(connectorRef));
+ return failure;
+ }
+
+ private static Exception stopStep(Exception failure,
ThrowingRunnable<Exception> step) {
+ try {
+ step.run();
+ } catch (Exception e) {
+ if (failure == null) {
+ return e;
}
- stopThreadPool(connectorRef);
- return null;
+ failure.addSuppressed(e);
}
- return connectorRef;
+ return failure;
}
private String getConnectorKey(HttpCommonEndpoint endpoint) {
@@ -1443,15 +1463,12 @@ public abstract class JettyHttpComponent extends
HttpCommonComponent
for (Map.Entry<String, ConnectorRef> connectorEntry :
CONNECTORS.entrySet()) {
ConnectorRef connectorRef = connectorEntry.getValue();
if (connectorRef != null && connectorRef.getRefCount() == 0) {
- connectorRef.server.removeConnector(connectorRef.connector);
- connectorRef.connector.stop();
- // Camel controls the lifecycle of these entities so remove the
- // 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());
+ Exception failure = stopConnector(connectorRef);
+ if (failure != null) {
+ LOG.warn("Error stopping Jetty connector: {}. This
exception is ignored.", connectorEntry.getKey(),
+ failure);
+ }
}
}
if (mbContainer != null) {
diff --git
a/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/JettyConsumerStopFailedTest.java
b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/JettyConsumerStopFailedTest.java
new file mode 100644
index 000000000000..c8e94f0543ca
--- /dev/null
+++
b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/JettyConsumerStopFailedTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 org.apache.camel.Consumer;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * When stopping the last consumer on a port fails (here Jetty fails to stop
its selectors on an interrupted thread, as
+ * when a route shutdown times out), the connector is still removed, so a new
route on the same port gets a new server
+ * that accepts connections.
+ */
+class JettyConsumerStopFailedTest extends BaseJettyTest {
+
+ private final HttpClient client =
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
+
+ @Test
+ void newRouteOnSamePortAnswersAfterFailedStop() throws Exception {
+ assertEquals("first", get("first"));
+
+ Consumer consumer = context.getRoute("first").getConsumer();
+ Thread.currentThread().interrupt();
+ try {
+ consumer.stop();
+ } catch (Exception e) {
+ // stopping may fail on an interrupted thread, which is what this
test is about
+ } finally {
+ Thread.interrupted();
+ }
+
assertFalse(JettyHttpComponent.CONNECTORS.containsKey("http:localhost:" +
getPort()),
+ "the connector of the stopped consumer is still registered");
+
+ context.getRouteController().stopRoute("first");
+ context.removeRoute("first");
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("jetty://http://localhost:{{port}}/second").routeId("second").setBody(constant("second"));
+ }
+ });
+
+ assertEquals("second", get("second"));
+ }
+
+ private String get(String path) throws Exception {
+ HttpRequest request =
HttpRequest.newBuilder(URI.create("http://localhost:" + getPort() + "/" + 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() {
+
from("jetty://http://localhost:{{port}}/first").routeId("first").setBody(constant("first"));
+ }
+ };
+ }
+}