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 002db3d8b2d0 CAMEL-26048: camel-mina - dispose the acceptor of a
consumer when it stops, so a route restart does not leak it (#28535)
002db3d8b2d0 is described below
commit 002db3d8b2d0626fcd30e5cae89dd19222599d40
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 27 15:41:32 2026 +0530
CAMEL-26048: camel-mina - dispose the acceptor of a consumer when it stops,
so a route restart does not leak it (#28535)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../apache/camel/component/mina/MinaConsumer.java | 72 ++++++--
.../component/mina/MinaConsumerRestartTest.java | 205 +++++++++++++++++++++
2 files changed, 261 insertions(+), 16 deletions(-)
diff --git
a/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaConsumer.java
b/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaConsumer.java
index 2573fe8ec14c..cb9350edaea4 100644
---
a/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaConsumer.java
+++
b/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaConsumer.java
@@ -71,6 +71,8 @@ public class MinaConsumer extends DefaultConsumer {
private IoAcceptor acceptor;
private final MinaConfiguration configuration;
private ExecutorService workerPool;
+ // the acceptor or connector and the worker pool were released by doStop,
and are created again on start
+ private boolean released;
public MinaConsumer(final MinaEndpoint endpoint, Processor processor)
throws Exception {
super(endpoint, processor);
@@ -81,6 +83,10 @@ public class MinaConsumer extends DefaultConsumer {
//
this.getEndpoint().setExchangePattern(ExchangePattern.InOut);
+ setupProtocol();
+ }
+
+ private void setupProtocol() throws Exception {
String protocol = configuration.getProtocol();
if (protocol.equals("tcp")) {
if (configuration.isClientMode()) {
@@ -104,6 +110,10 @@ public class MinaConsumer extends DefaultConsumer {
@Override
protected void doStart() throws Exception {
super.doStart();
+ if (released) {
+ setupProtocol();
+ released = false;
+ }
if (configuration.isClientMode() &&
configuration.getProtocol().equals("tcp")) {
connector.setHandler(new ReceiveHandler());
ConnectFuture future = connector.connect(address);
@@ -120,26 +130,44 @@ public class MinaConsumer extends DefaultConsumer {
@Override
protected void doStop() throws Exception {
- if (configuration.isClientMode() &&
configuration.getProtocol().equals("tcp")) {
- LOG.info("Disconnect from server address: {} using connector: {}",
address, connector);
- if (session != null) {
- CloseFuture closeFuture = session.closeNow();
- closeFuture.awaitUninterruptibly();
- }
- connector.dispose(true);
- } else {
- LOG.info("Unbinding from server address: {} using acceptor: {}",
address, acceptor);
- if (address instanceof InetSocketAddress) {
- // need to check if the address is IPV4 all network address
- if ("0.0.0.0".equals(((InetSocketAddress)
address).getAddress().getHostAddress())) {
- LOG.info("Unbind the server address {}",
acceptor.getLocalAddresses());
- acceptor.unbind(acceptor.getLocalAddresses());
+ try {
+ if (configuration.isClientMode() &&
configuration.getProtocol().equals("tcp")) {
+ LOG.info("Disconnect from server address: {} using connector:
{}", address, connector);
+ if (session != null) {
+ CloseFuture closeFuture = session.closeNow();
+ closeFuture.awaitUninterruptibly();
+ }
+ } else {
+ LOG.info("Unbinding from server address: {} using acceptor:
{}", address, acceptor);
+ if (address instanceof InetSocketAddress) {
+ // need to check if the address is IPV4 all network address
+ if ("0.0.0.0".equals(((InetSocketAddress)
address).getAddress().getHostAddress())) {
+ LOG.info("Unbind the server address {}",
acceptor.getLocalAddresses());
+ acceptor.unbind(acceptor.getLocalAddresses());
+ } else {
+ acceptor.unbind(address);
+ }
} else {
acceptor.unbind(address);
}
- } else {
- acceptor.unbind(address);
}
+ } finally {
+ // a route restart creates a new consumer and never shuts this one
down (doShutdown), so release the
+ // connector or acceptor (selectors, I/O threads) and the worker
pool now, also when the disconnect or
+ // unbind failed; doStart creates them again if this consumer is
started again. The address is already
+ // unbound, so only wait for the I/O threads of the acceptor to
end when the CamelContext stops, as
+ // doShutdown did before (a datagram acceptor takes about two
seconds to end); once disposed, the dispose
+ // in doShutdown does nothing
+ if (connector != null) {
+ connector.dispose(true);
+ }
+ if (acceptor != null) {
+ acceptor.dispose(getEndpoint().getCamelContext().isStopping());
+ }
+ if (workerPool != null) {
+ workerPool.shutdown();
+ }
+ released = true;
}
super.doStop();
}
@@ -385,6 +413,18 @@ public class MinaConsumer extends DefaultConsumer {
return acceptor;
}
+ /**
+ * The connector of a consumer in client mode (for tests).
+ */
+ IoConnector getConnector() {
+ return connector;
+ }
+
+ /**
+ * Sets the acceptor this consumer binds when it starts. The consumer owns
it from then on: it is disposed when the
+ * consumer stops, and a start of the same consumer after a stop creates a
new acceptor from the configuration
+ * instead of binding this one again.
+ */
public void setAcceptor(IoAcceptor acceptor) {
this.acceptor = acceptor;
}
diff --git
a/components/camel-mina/src/test/java/org/apache/camel/component/mina/MinaConsumerRestartTest.java
b/components/camel-mina/src/test/java/org/apache/camel/component/mina/MinaConsumerRestartTest.java
new file mode 100644
index 000000000000..d40d70ce0e6c
--- /dev/null
+++
b/components/camel-mina/src/test/java/org/apache/camel/component/mina/MinaConsumerRestartTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.mina;
+
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.mina.core.service.IoAcceptor;
+import org.apache.mina.core.service.IoConnector;
+import org.apache.mina.core.service.IoService;
+import org.apache.mina.filter.executor.ExecutorFilter;
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * A route stop/start creates a new MinaConsumer, and the stopped one is never
shut down: its acceptor (selectors, I/O
+ * threads) and worker pool must be released when it stops.
+ */
+class MinaConsumerRestartTest extends BaseMinaTest {
+
+ private final int udpPort = getNextPort();
+ private final int vmPort = getNextPort();
+
+ @Test
+ void routeRestartReleasesTheAcceptorOfTheStoppedTcpConsumer() throws
Exception {
+ for (IoAcceptor acceptor : restart("tcp", 3)) {
+ assertReleased(acceptor);
+ }
+ // the consumer of the restarted route works
+ sendAndExpect("mina:tcp://localhost:" + getPort() +
"?sync=false&textline=true");
+ }
+
+ @Test
+ void routeRestartReleasesTheAcceptorOfTheStoppedUdpConsumer() throws
Exception {
+ addUdpRoute();
+ for (IoAcceptor acceptor : restart("udp", 3)) {
+ assertReleased(acceptor);
+ }
+ sendAndExpect("mina:udp://localhost:" + udpPort + "?sync=false");
+ }
+
+ @Test
+ void routeRestartReleasesTheAcceptorOfTheStoppedVmConsumer() throws
Exception {
+ for (IoAcceptor acceptor : restart("vm", 3)) {
+ assertTrue(acceptor.isDisposed(), "the acceptor of a stopped
consumer must be disposed");
+ }
+ sendAndExpect("mina:vm://localhost:" + vmPort +
"?sync=false&textline=true");
+ }
+
+ @Test
+ void theSameConsumerCanBeStartedAgain() throws Exception {
+ // a consumer that is stopped and started again (not replaced) creates
a new acceptor
+ MinaConsumer consumer = (MinaConsumer)
context.getRoute("tcp").getConsumer();
+ IoAcceptor first = consumer.getAcceptor();
+
+ consumer.stop();
+ consumer.start();
+
+ assertTrue(first.isDisposed());
+ assertNotSame(first, consumer.getAcceptor());
+ assertFalse(consumer.getAcceptor().isDisposed());
+ sendAndExpect("mina:tcp://localhost:" + getPort() +
"?sync=false&textline=true");
+ }
+
+ @Test
+ void theSameClientModeConsumerCanBeStartedAgain() throws Exception {
+ // a consumer in client mode (connected to the tcp route) that is
stopped and started again creates a new
+ // connector and connects again; before, the start failed as the
stopped connector was disposed
+ context.getRouteController().startRoute("client");
+ MinaConsumer consumer = (MinaConsumer)
context.getRoute("client").getConsumer();
+ IoConnector first = consumer.getConnector();
+
+ consumer.stop();
+ consumer.start();
+
+ assertReleased(first);
+ assertNotSame(first, consumer.getConnector());
+ assertFalse(consumer.getConnector().isDisposed());
+ assertEquals(1, consumer.getConnector().getManagedSessionCount(), "the
restarted consumer is connected");
+ }
+
+ @Test
+ void theAcceptorIsReleasedWhenTheUnbindFails() throws Exception {
+ // a failing unbind must not skip the release: the acceptor and the
worker pool are released, and a start of the
+ // same consumer creates a new acceptor instead of binding the old one
again
+ MinaConsumer consumer = (MinaConsumer)
context.getRoute("tcp").getConsumer();
+ IoAcceptor first = consumer.getAcceptor();
+ IoAcceptor failing = mock(IoAcceptor.class, delegatesTo(first));
+ doThrow(new IllegalStateException("unbind
failed")).when(failing).unbind(any(SocketAddress.class));
+ consumer.setAcceptor(failing);
+
+ Exception e = assertThrows(IllegalStateException.class,
consumer::stop);
+ assertEquals("unbind failed", e.getMessage());
+
+ assertReleased(first);
+ consumer.start();
+ assertNotSame(failing, consumer.getAcceptor());
+ sendAndExpect("mina:tcp://localhost:" + getPort() +
"?sync=false&textline=true");
+ }
+
+ @Test
+ void onlyACamelContextStopWaitsForTheIoThreads() throws Exception {
+ // the address is already unbound, so a route stop does not wait for
the I/O threads of the acceptor (a datagram
+ // acceptor can take seconds to end); a CamelContext stop waits for
them, as doShutdown did before
+ addUdpRoute();
+ IoAcceptor routeStopped = watchAcceptor("udp");
+ context.getRouteController().stopRoute("udp");
+ verify(routeStopped).dispose(false);
+ verify(routeStopped, never()).dispose(true);
+
+ context.getRouteController().startRoute("udp");
+ IoAcceptor contextStopped = watchAcceptor("udp");
+ context.stop();
+ verify(contextStopped, never()).dispose(false);
+ // doShutdown disposes again, which does nothing then
+ verify(contextStopped, atLeastOnce()).dispose(true);
+ }
+
+ private void addUdpRoute() throws Exception {
+ // only in the udp tests: a datagram acceptor takes seconds to end,
and the CamelContext stop waits for it
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ fromF("mina:udp://localhost:%d?sync=false",
udpPort).routeId("udp").to("mock:result");
+ }
+ });
+ }
+
+ private IoAcceptor watchAcceptor(String routeId) {
+ // the acceptor of the consumer, behind a mock that passes every call
on to it
+ MinaConsumer consumer = (MinaConsumer)
context.getRoute(routeId).getConsumer();
+ IoAcceptor acceptor = mock(IoAcceptor.class,
delegatesTo(consumer.getAcceptor()));
+ consumer.setAcceptor(acceptor);
+ return acceptor;
+ }
+
+ private static void assertReleased(IoService service) {
+ assertTrue(service.isDisposed(), "the acceptor of a stopped consumer
must be disposed");
+ ExecutorFilter workers = (ExecutorFilter)
service.getFilterChain().get("threadPool");
+ assertTrue(((ExecutorService) workers.getExecutor()).isShutdown(),
+ "the worker pool of a stopped consumer must be shut down");
+ }
+
+ private List<IoAcceptor> restart(String routeId, int times) throws
Exception {
+ List<IoAcceptor> stopped = new ArrayList<>();
+ for (int i = 0; i < times; i++) {
+ stopped.add(((MinaConsumer)
context.getRoute(routeId).getConsumer()).getAcceptor());
+ context.getRouteController().stopRoute(routeId);
+ context.getRouteController().startRoute(routeId);
+ }
+ assertNotSame(stopped.get(times - 1), ((MinaConsumer)
context.getRoute(routeId).getConsumer()).getAcceptor(),
+ "a route restart creates a new consumer");
+ return stopped;
+ }
+
+ private void sendAndExpect(String uri) throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.expectedBodiesReceived("hello");
+ template.sendBody(uri, "hello");
+ mock.assertIsSatisfied();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ fromF("mina:tcp://localhost:%d?sync=false&textline=true",
getPort()).routeId("tcp").to("mock:result");
+ fromF("mina:vm://localhost:%d?sync=false&textline=true",
vmPort).routeId("vm").to("mock:result");
+
fromF("mina:tcp://localhost:%d?sync=false&textline=true&clientMode=true",
getPort()).routeId("client")
+ .autoStartup(false).to("mock:client");
+ }
+ };
+ }
+}