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 35800edf39f8 CAMEL-26050: camel-thrift - stop the TAsyncClientManager 
of the asynchronous producer when it stops (#28537)
35800edf39f8 is described below

commit 35800edf39f8fb1cbb59091a81eaa4d07105d8d0
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 27 19:06:54 2026 +0530

    CAMEL-26050: camel-thrift - stop the TAsyncClientManager of the 
asynchronous producer when it stops (#28537)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/component/thrift/ThriftProducer.java     | 136 ++++++++-
 .../apache/camel/component/thrift/ThriftUtils.java |  27 +-
 .../ThriftProducerAsyncInFlightStopTest.java       | 308 +++++++++++++++++++++
 .../thrift/ThriftProducerAsyncStopTest.java        |  67 +++++
 4 files changed, 524 insertions(+), 14 deletions(-)

diff --git 
a/components/camel-thrift/src/main/java/org/apache/camel/component/thrift/ThriftProducer.java
 
b/components/camel-thrift/src/main/java/org/apache/camel/component/thrift/ThriftProducer.java
index 1ee34cafd47a..73cb2b3bed73 100644
--- 
a/components/camel-thrift/src/main/java/org/apache/camel/component/thrift/ThriftProducer.java
+++ 
b/components/camel-thrift/src/main/java/org/apache/camel/component/thrift/ThriftProducer.java
@@ -17,9 +17,13 @@
 package org.apache.camel.component.thrift;
 
 import java.io.IOException;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.camel.AsyncCallback;
 import org.apache.camel.AsyncProducer;
+import org.apache.camel.CamelExchangeException;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
 import org.apache.camel.component.thrift.client.AsyncClientMethodCallback;
@@ -28,6 +32,7 @@ import org.apache.camel.support.ResourceHelper;
 import org.apache.camel.support.jsse.SSLContextParameters;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.thrift.TException;
+import org.apache.thrift.async.TAsyncClientManager;
 import org.apache.thrift.transport.TNonblockingSocket;
 import org.apache.thrift.transport.TNonblockingTransport;
 import org.apache.thrift.transport.TSSLTransportFactory;
@@ -48,7 +53,10 @@ public class ThriftProducer extends DefaultAsyncProducer 
implements AsyncProduce
     protected final ThriftEndpoint endpoint;
     private TTransport syncTransport;
     private TNonblockingTransport asyncTransport;
-    private Object thriftClient;
+    private TAsyncClientManager asyncClientManager;
+    private volatile Object thriftClient;
+    // the callbacks of the asynchronous calls that have not completed yet, 
failed when this producer stops
+    private final Set<PendingCallback> pendingCallbacks = 
ConcurrentHashMap.newKeySet();
 
     public ThriftProducer(ThriftEndpoint endpoint, ThriftConfiguration 
configuration) {
         super(endpoint);
@@ -60,10 +68,23 @@ public class ThriftProducer extends DefaultAsyncProducer 
implements AsyncProduce
     public boolean process(Exchange exchange, AsyncCallback callback) {
         Message message = exchange.getIn();
 
+        PendingCallback pending = new PendingCallback(exchange, callback);
+        pendingCallbacks.add(pending);
+        // read after the registration: doStop clears the client before it 
fails the pending calls
+        Object client = thriftClient;
+        if (client == null) {
+            // the producer is stopped
+            pending.stopped();
+            return false;
+        }
         try {
-            ThriftUtils.invokeAsyncMethod(thriftClient, 
configuration.getMethod(), message.getBody(),
-                    new AsyncClientMethodCallback(exchange, callback));
+            ThriftUtils.invokeAsyncMethod(client, configuration.getMethod(), 
message.getBody(), pending);
         } catch (Exception e) {
+            // the call was not made
+            if (!pending.discard()) {
+                // a stop of the producer completed the exchange meanwhile, 
with done(false)
+                return false;
+            }
             if (e.getCause() instanceof TException) {
                 exchange.setException(e.getCause());
             } else {
@@ -72,6 +93,12 @@ public class ThriftProducer extends DefaultAsyncProducer 
implements AsyncProduce
             callback.done(true);
             return true;
         }
+        if (client != thriftClient) {
+            // the producer stopped meanwhile, and may have started again with 
a new client: the stop may have failed
+            // the pending calls before this one was registered, and the 
selector thread of the stopped client manager,
+            // which may still be ending, accepts the call without ever 
completing it
+            pending.stopped();
+        }
 
         return false;
     }
@@ -124,27 +151,112 @@ public class ThriftProducer extends DefaultAsyncProducer 
implements AsyncProduce
             if (asyncTransport == null) {
                 initializeAsyncTransport();
                 LOG.info("Getting asynchronous client implementation");
+                asyncClientManager = new TAsyncClientManager();
                 thriftClient = 
ThriftUtils.constructAsyncClientInstance(endpoint.getServicePackage(), 
endpoint.getServiceName(),
                         asyncTransport,
-                        configuration.getExchangeProtocol(), 
endpoint.getCamelContext());
+                        configuration.getExchangeProtocol(), 
endpoint.getCamelContext(), asyncClientManager);
             }
         }
     }
 
     @Override
     protected void doStop() throws Exception {
-        if (syncTransport != null) {
-            LOG.debug("Terminating synchronous transport the remote Thrift 
server");
-            syncTransport.close();
-            syncTransport = null;
-        } else if (asyncTransport != null) {
-            LOG.debug("Terminating asynchronous transport the remote Thrift 
server");
-            asyncTransport.close();
-            asyncTransport = null;
+        try {
+            if (syncTransport != null) {
+                LOG.debug("Terminating synchronous transport the remote Thrift 
server");
+                syncTransport.close();
+                syncTransport = null;
+            } else if (asyncTransport != null) {
+                // before the pending calls are failed: process() fails a call 
whose client was cleared or replaced
+                thriftClient = null;
+                LOG.debug("Terminating asynchronous transport the remote 
Thrift server");
+                asyncTransport.close();
+                asyncTransport = null;
+            }
+        } finally {
+            if (asyncClientManager != null) {
+                // stops the selector thread and closes the selector of the 
async client
+                asyncClientManager.stop();
+                asyncClientManager = null;
+            }
+            // after the stop, so that a call registered meanwhile is failed 
too
+            failPendingCallbacks();
         }
         super.doStop();
     }
 
+    /**
+     * Fails the exchanges of the asynchronous calls that have not completed: 
the stopped selector thread does not
+     * complete them. A call that completes meanwhile is completed once, and a 
continuation that throws does not skip
+     * the other calls.
+     */
+    private void failPendingCallbacks() {
+        for (PendingCallback pending : pendingCallbacks) {
+            try {
+                pending.stopped();
+            } catch (Exception e) {
+                LOG.warn("Error completing the exchange of a pending call of 
method {} when stopping the Thrift producer",
+                        configuration.getMethod(), e);
+            }
+        }
+    }
+
+    /**
+     * The number of asynchronous calls that have not completed yet (for 
tests).
+     */
+    int getPendingCallbackCount() {
+        return pendingCallbacks.size();
+    }
+
+    /**
+     * Completes the exchange of an asynchronous call once: with the response 
or error of the call, or with an error
+     * when the producer stops first.
+     */
+    private final class PendingCallback extends AsyncClientMethodCallback {
+        private final Exchange exchange;
+        private final AtomicBoolean done = new AtomicBoolean();
+
+        PendingCallback(Exchange exchange, AsyncCallback callback) {
+            super(exchange, callback);
+            this.exchange = exchange;
+        }
+
+        @Override
+        public void onComplete(Object response) {
+            if (finish()) {
+                super.onComplete(response);
+            }
+        }
+
+        @Override
+        public void onError(Exception exception) {
+            if (finish()) {
+                super.onError(exception);
+            }
+        }
+
+        void stopped() {
+            if (finish()) {
+                super.onError(new CamelExchangeException(
+                        "The Thrift producer was stopped before the call of 
method " + configuration.getMethod()
+                                                         + " completed",
+                        exchange));
+            }
+        }
+
+        boolean discard() {
+            return finish();
+        }
+
+        private boolean finish() {
+            if (done.compareAndSet(false, true)) {
+                pendingCallbacks.remove(this);
+                return true;
+            }
+            return false;
+        }
+    }
+
     protected void initializeSyncTransport() throws TTransportException {
         if (!ObjectHelper.isEmpty(configuration.getHost()) && 
!ObjectHelper.isEmpty(configuration.getPort())) {
             LOG.info("Creating transport to the remote Thrift server {}:{}", 
configuration.getHost(), configuration.getPort());
diff --git 
a/components/camel-thrift/src/main/java/org/apache/camel/component/thrift/ThriftUtils.java
 
b/components/camel-thrift/src/main/java/org/apache/camel/component/thrift/ThriftUtils.java
index 86a75308d885..aff764d9b6af 100644
--- 
a/components/camel-thrift/src/main/java/org/apache/camel/component/thrift/ThriftUtils.java
+++ 
b/components/camel-thrift/src/main/java/org/apache/camel/component/thrift/ThriftUtils.java
@@ -87,10 +87,33 @@ public final class ThriftUtils {
         return clientInstance;
     }
 
+    /**
+     * Constructs an async client with a new client manager, which the caller 
cannot stop: its selector thread and
+     * selector stay until the JVM exits.
+     *
+     * @deprecated use the overload with a {@link TAsyncClientManager} 
argument, and stop that manager when the client
+     *             is no longer used.
+     */
+    @Deprecated(since = "4.24.0")
     @SuppressWarnings({ "rawtypes", "unchecked" })
     public static Object constructAsyncClientInstance(
             String packageName, String serviceName, TNonblockingTransport 
transport, ThriftExchangeProtocol exchangeProtocol,
             final CamelContext context) {
+        try {
+            return constructAsyncClientInstance(packageName, serviceName, 
transport, exchangeProtocol, context,
+                    new TAsyncClientManager());
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Thrift async client manager 
cannot be created", e);
+        }
+    }
+
+    /**
+     * Constructs an async client that uses the given client manager; the 
caller owns the manager and must stop it.
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public static Object constructAsyncClientInstance(
+            String packageName, String serviceName, TNonblockingTransport 
transport, ThriftExchangeProtocol exchangeProtocol,
+            final CamelContext context, TAsyncClientManager clientManager) {
         Object asynClientInstance = null;
         Class[] getterParamTypes = { TNonblockingTransport.class };
         Class[] constructorParamTypes = { TAsyncClientManager.class, 
TProtocolFactory.class };
@@ -101,7 +124,7 @@ public final class ThriftUtils {
             Class clientClass = 
context.getClassResolver().resolveMandatoryClass(clientClassName);
             Constructor factoryConstructor = 
clientClass.getConstructor(constructorParamTypes);
             Object factoryInstance
-                    = factoryConstructor.newInstance(new 
TAsyncClientManager(), constructAsyncProtocol(exchangeProtocol));
+                    = factoryConstructor.newInstance(clientManager, 
constructAsyncProtocol(exchangeProtocol));
             Method asyncClientGetter = 
ReflectionHelper.findMethod(clientClass, 
ThriftConstants.THRIFT_ASYNC_CLIENT_GETTER_NAME,
                     getterParamTypes);
             if (asyncClientGetter == null) {
@@ -116,7 +139,7 @@ public final class ThriftUtils {
         } catch (NoSuchMethodException e) {
             throw new IllegalArgumentException("Thrift sync client factory 
class not found: " + clientClassName);
         } catch (InstantiationException | IllegalAccessException | 
IllegalArgumentException | InvocationTargetException
-                 | IOException | SecurityException e) {
+                 | SecurityException e) {
             throw new IllegalArgumentException(e);
         }
         return asynClientInstance;
diff --git 
a/components/camel-thrift/src/test/java/org/apache/camel/component/thrift/ThriftProducerAsyncInFlightStopTest.java
 
b/components/camel-thrift/src/test/java/org/apache/camel/component/thrift/ThriftProducerAsyncInFlightStopTest.java
new file mode 100644
index 000000000000..ead635146885
--- /dev/null
+++ 
b/components/camel-thrift/src/test/java/org/apache/camel/component/thrift/ThriftProducerAsyncInFlightStopTest.java
@@ -0,0 +1,308 @@
+/*
+ * 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.thrift;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelExchangeException;
+import org.apache.camel.Exchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.thrift.TException;
+import org.apache.thrift.async.AsyncMethodCallback;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Stopping the client manager of the asynchronous producer ends its selector 
thread without completing the calls it
+ * still has: the producer must fail their exchanges when it stops, else they 
never complete.
+ */
+class ThriftProducerAsyncInFlightStopTest extends CamelTestSupport {
+
+    @Test
+    void stoppingTheProducerFailsAnInFlightCall() throws Exception {
+        CountDownLatch requestReceived = new CountDownLatch(1);
+        try (ServerSocket server = new ServerSocket(0)) {
+            startSilentServer(server, requestReceived);
+
+            String uri = pingUri(server.getLocalPort());
+            ThriftProducer producer = (ThriftProducer) 
context.getEndpoint(uri).createProducer();
+            producer.start();
+            try {
+                Exchange exchange = context.getEndpoint(uri).createExchange();
+                CountDownLatch done = new CountDownLatch(1);
+                producer.process(exchange, doneSync -> done.countDown());
+                assertTrue(requestReceived.await(10, TimeUnit.SECONDS), "the 
call must be sent");
+                assertEquals(1, producer.getPendingCallbackCount());
+
+                producer.stop();
+
+                assertTrue(done.await(10, TimeUnit.SECONDS), "the exchange of 
the call must complete");
+                assertStoppedBeforeTheCallCompleted(exchange);
+                assertEquals(0, producer.getPendingCallbackCount());
+            } finally {
+                producer.stop();
+            }
+        }
+    }
+
+    @Test
+    void aContinuationThatThrowsDoesNotKeepTheSelectorThreadRunning() throws 
Exception {
+        CountDownLatch requestReceived = new CountDownLatch(1);
+        try (ServerSocket server = new ServerSocket(0)) {
+            startSilentServer(server, requestReceived);
+
+            String uri = pingUri(server.getLocalPort());
+            ThriftProducer producer = (ThriftProducer) 
context.getEndpoint(uri).createProducer();
+            Set<Thread> before = Thread.getAllStackTraces().keySet();
+            producer.start();
+            try {
+                List<Thread> selectors = 
Thread.getAllStackTraces().keySet().stream()
+                        .filter(t -> !before.contains(t) && 
t.getName().startsWith("TAsyncClientManager#SelectorThread"))
+                        .toList();
+                assertEquals(1, selectors.size(), "the selector thread of the 
producer");
+
+                Exchange exchange = context.getEndpoint(uri).createExchange();
+                CountDownLatch done = new CountDownLatch(1);
+                producer.process(exchange, doneSync -> {
+                    done.countDown();
+                    throw new IllegalStateException("the continuation of the 
exchange fails");
+                });
+                assertTrue(requestReceived.await(10, TimeUnit.SECONDS), "the 
call must be sent");
+
+                // the continuation runs on the stopping thread and throws
+                assertDoesNotThrow(producer::stop);
+
+                assertTrue(done.await(10, TimeUnit.SECONDS), "the exchange of 
the call must complete");
+                assertStoppedBeforeTheCallCompleted(exchange);
+                assertEquals(0, producer.getPendingCallbackCount());
+                selectors.get(0).join(TimeUnit.SECONDS.toMillis(10));
+                assertFalse(selectors.get(0).isAlive(), "the selector thread 
must end");
+            } finally {
+                producer.stop();
+            }
+        }
+    }
+
+    @Test
+    void aCompletedCallIsNotPending() throws Exception {
+        // control: the callback of a call that completed (here with an error: 
nothing listens) is not kept
+        int port;
+        try (ServerSocket closed = new ServerSocket(0)) {
+            port = closed.getLocalPort();
+        }
+        String uri = pingUri(port);
+        ThriftProducer producer = (ThriftProducer) 
context.getEndpoint(uri).createProducer();
+        producer.start();
+        try {
+            Exchange exchange = context.getEndpoint(uri).createExchange();
+            CountDownLatch done = new CountDownLatch(1);
+            producer.process(exchange, doneSync -> done.countDown());
+
+            assertTrue(done.await(10, TimeUnit.SECONDS));
+            assertTrue(exchange.isFailed());
+            assertEquals(0, producer.getPendingCallbackCount());
+        } finally {
+            producer.stop();
+        }
+    }
+
+    @Test
+    void aCallThatFailsToStartWhileTheProducerStopsIsCompletedOnce() throws 
Exception {
+        ThriftProducer producer = startProducerOnClosedPort();
+        try {
+            // the stop runs between the registration of the call and the 
failure to start it: it completes the
+            // exchange (asynchronously), so the failure must not complete it 
again
+            setClient(producer, new StoppingClient(producer));
+            Exchange exchange = producer.getEndpoint().createExchange();
+            List<Boolean> done = new CopyOnWriteArrayList<>();
+
+            boolean sync = producer.process(exchange, done::add);
+
+            assertEquals(List.of(false), done, "the exchange must complete 
once, asynchronously");
+            assertFalse(sync, "process must return the doneSync passed to the 
callback");
+            assertStoppedBeforeTheCallCompleted(exchange);
+            assertEquals(0, producer.getPendingCallbackCount());
+        } finally {
+            producer.stop();
+        }
+    }
+
+    @Test
+    void aCallThatFailsToStartIsCompletedSynchronously() throws Exception {
+        // control: without a stop, the failure to start the call completes 
the exchange synchronously
+        ThriftProducer producer = startProducerOnClosedPort();
+        try {
+            setClient(producer, new FailingClient());
+            Exchange exchange = producer.getEndpoint().createExchange();
+            List<Boolean> done = new CopyOnWriteArrayList<>();
+
+            boolean sync = producer.process(exchange, done::add);
+
+            assertEquals(List.of(true), done);
+            assertTrue(sync);
+            assertInstanceOf(TException.class, exchange.getException());
+            assertEquals(0, producer.getPendingCallbackCount());
+        } finally {
+            producer.stop();
+        }
+    }
+
+    @Test
+    void aCallAcceptedAfterTheProducerFailedItsPendingCallsIsFailed() throws 
Exception {
+        aCallTheStopDoesNotSeeIsFailed(false);
+    }
+
+    @Test
+    void 
aCallAcceptedAfterTheProducerFailedItsPendingCallsIsFailedAlsoWhenTheProducerStartsAgain()
 throws Exception {
+        aCallTheStopDoesNotSeeIsFailed(true);
+    }
+
+    private void aCallTheStopDoesNotSeeIsFailed(boolean startAgain) throws 
Exception {
+        ThriftProducer producer = startProducerOnClosedPort();
+        try {
+            // the stop fails the pending calls while this call is being 
registered, so it does not see it, and a
+            // selector thread that is still ending accepts the call and never 
completes it
+            setClient(producer, new UnseenByTheStopClient(producer, 
startAgain));
+            Exchange exchange = producer.getEndpoint().createExchange();
+            List<Boolean> done = new CopyOnWriteArrayList<>();
+
+            boolean sync = producer.process(exchange, done::add);
+
+            assertEquals(List.of(false), done, "the exchange must complete 
once");
+            assertFalse(sync);
+            assertStoppedBeforeTheCallCompleted(exchange);
+            assertEquals(0, producer.getPendingCallbackCount());
+        } finally {
+            producer.stop();
+        }
+    }
+
+    @Test
+    void aCallToAStoppedProducerIsFailed() throws Exception {
+        ThriftProducer producer = startProducerOnClosedPort();
+        producer.stop();
+
+        Exchange exchange = producer.getEndpoint().createExchange();
+        List<Boolean> done = new CopyOnWriteArrayList<>();
+        boolean sync = producer.process(exchange, done::add);
+
+        assertEquals(List.of(false), done, "the exchange must complete once");
+        assertFalse(sync);
+        assertStoppedBeforeTheCallCompleted(exchange);
+        assertEquals(0, producer.getPendingCallbackCount());
+    }
+
+    private ThriftProducer startProducerOnClosedPort() throws Exception {
+        int port;
+        try (ServerSocket closed = new ServerSocket(0)) {
+            port = closed.getLocalPort();
+        }
+        ThriftProducer producer = (ThriftProducer) 
context.getEndpoint(pingUri(port)).createProducer();
+        producer.start();
+        return producer;
+    }
+
+    private static void setClient(ThriftProducer producer, Object client) 
throws Exception {
+        Field field = ThriftProducer.class.getDeclaredField("thriftClient");
+        field.setAccessible(true);
+        field.set(producer, client);
+    }
+
+    // stops the producer, then fails to start the call as a stopped client 
manager does
+    public static class StoppingClient {
+        private final ThriftProducer producer;
+
+        StoppingClient(ThriftProducer producer) {
+            this.producer = producer;
+        }
+
+        public void ping(AsyncMethodCallback<Void> resultHandler) throws 
TException {
+            producer.stop();
+            throw new TException("SelectThread is not running");
+        }
+    }
+
+    public static class FailingClient {
+        public void ping(AsyncMethodCallback<Void> resultHandler) throws 
TException {
+            throw new TException("SelectThread is not running");
+        }
+    }
+
+    // accepts the call and never completes it, while the producer stops 
without seeing the call (and may start again)
+    public static class UnseenByTheStopClient {
+        private final ThriftProducer producer;
+        private final boolean startAgain;
+
+        UnseenByTheStopClient(ThriftProducer producer, boolean startAgain) {
+            this.producer = producer;
+            this.startAgain = startAgain;
+        }
+
+        public void ping(AsyncMethodCallback<Void> resultHandler) throws 
Exception {
+            // the stop fails the pending calls before this one is registered
+            Field field = 
ThriftProducer.class.getDeclaredField("pendingCallbacks");
+            field.setAccessible(true);
+            ((Set<?>) field.get(producer)).clear();
+            producer.stop();
+            if (startAgain) {
+                producer.start();
+            }
+        }
+    }
+
+    private static String pingUri(int port) {
+        return "thrift://localhost:" + port + 
"/org.apache.camel.component.thrift.generated.Calculator?method=ping";
+    }
+
+    // a server that reads the request and never answers
+    private static void startSilentServer(ServerSocket server, CountDownLatch 
requestReceived) {
+        Thread silent = new Thread(() -> {
+            try (Socket socket = server.accept(); InputStream in = 
socket.getInputStream()) {
+                if (in.read() != -1) {
+                    requestReceived.countDown();
+                }
+                while (in.read() != -1) {
+                    // keep the connection open until the client closes it
+                }
+            } catch (Exception e) {
+                // the test ends
+            }
+        });
+        silent.setDaemon(true);
+        silent.start();
+    }
+
+    private static void assertStoppedBeforeTheCallCompleted(Exchange exchange) 
{
+        assertInstanceOf(CamelExchangeException.class, 
exchange.getException());
+        assertTrue(exchange.getException().getMessage().startsWith(
+                "The Thrift producer was stopped before the call of method 
ping completed"),
+                exchange.getException().getMessage());
+    }
+}
diff --git 
a/components/camel-thrift/src/test/java/org/apache/camel/component/thrift/ThriftProducerAsyncStopTest.java
 
b/components/camel-thrift/src/test/java/org/apache/camel/component/thrift/ThriftProducerAsyncStopTest.java
new file mode 100644
index 000000000000..6e2257193354
--- /dev/null
+++ 
b/components/camel-thrift/src/test/java/org/apache/camel/component/thrift/ThriftProducerAsyncStopTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.thrift;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * The asynchronous producer (the default, synchronous=false) creates a 
TAsyncClientManager (a selector thread and a
+ * selector) on every start; stopping the producer must stop it.
+ */
+class ThriftProducerAsyncStopTest extends ThriftProducerBaseTest {
+
+    @Test
+    void routeRestartsStopTheClientManagersOfTheStoppedProducers() throws 
Exception {
+        Set<Thread> before = Thread.getAllStackTraces().keySet();
+        for (int i = 0; i < 5; i++) {
+            context.getRouteController().stopRoute("r");
+            context.getRouteController().startRoute("r");
+            // the asynchronous producer of the restarted route works
+            assertNull(template.requestBody("direct:thrift-ping", (Object) 
null));
+        }
+        context.getRouteController().stopRoute("r");
+
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            long alive = Thread.getAllStackTraces().keySet().stream()
+                    .filter(t -> !before.contains(t) && t.isAlive()
+                            && 
t.getName().startsWith("TAsyncClientManager#SelectorThread"))
+                    .count();
+            assertEquals(0, alive, "selector threads of stopped asynchronous 
producers");
+        });
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:thrift-ping").routeId("r")
+                        .to("thrift://localhost:" + thriftTestPort
+                            + 
"/org.apache.camel.component.thrift.generated.Calculator?method=ping");
+            }
+        };
+    }
+}

Reply via email to