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 a78333f0698a CAMEL-26049: camel-coap - destroy the CoAP servers when 
the component stops, and the client endpoint of a producer when it stops 
(#28536)
a78333f0698a is described below

commit a78333f0698a4b45c2a7b677662191015af3aa5c
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 27 15:41:59 2026 +0530

    CAMEL-26049: camel-coap - destroy the CoAP servers when the component 
stops, and the client endpoint of a producer when it stops (#28536)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../java/org/apache/camel/coap/CoAPComponent.java  |   5 +-
 .../java/org/apache/camel/coap/CoAPProducer.java   |  36 ++++++--
 .../coap/CoAPComponentStopDestroysServersTest.java | 102 +++++++++++++++++++++
 .../camel/coap/CoAPProducerNoResponseTest.java     |  61 ++++++++++++
 .../coap/CoAPProducerStopDestroysEndpointTest.java |  63 +++++++++++++
 5 files changed, 257 insertions(+), 10 deletions(-)

diff --git 
a/components/camel-coap/src/main/java/org/apache/camel/coap/CoAPComponent.java 
b/components/camel-coap/src/main/java/org/apache/camel/coap/CoAPComponent.java
index 19adfe6da90b..4a1d41cf588b 100644
--- 
a/components/camel-coap/src/main/java/org/apache/camel/coap/CoAPComponent.java
+++ 
b/components/camel-coap/src/main/java/org/apache/camel/coap/CoAPComponent.java
@@ -304,9 +304,12 @@ public class CoAPComponent extends DefaultComponent 
implements RestConsumerFacto
 
     @Override
     protected void doStop() throws Exception {
+        // destroy (not only stop) the servers: stop() keeps their executors, 
whose threads are not daemon threads
+        // and would keep the JVM running after the CamelContext is stopped; a 
later start creates them again
         for (CoapServer s : servers.values()) {
-            s.stop();
+            s.destroy();
         }
+        servers.clear();
         super.doStop();
     }
 }
diff --git 
a/components/camel-coap/src/main/java/org/apache/camel/coap/CoAPProducer.java 
b/components/camel-coap/src/main/java/org/apache/camel/coap/CoAPProducer.java
index c563fceb47d3..e539607eaf44 100644
--- 
a/components/camel-coap/src/main/java/org/apache/camel/coap/CoAPProducer.java
+++ 
b/components/camel-coap/src/main/java/org/apache/camel/coap/CoAPProducer.java
@@ -26,13 +26,16 @@ import org.apache.camel.util.URISupport;
 import org.eclipse.californium.core.CoapClient;
 import org.eclipse.californium.core.CoapResponse;
 import org.eclipse.californium.core.coap.MediaTypeRegistry;
+import org.eclipse.californium.core.network.Endpoint;
 
 /**
  * The CoAP producer.
  */
 public class CoAPProducer extends DefaultProducer {
     private final CoAPEndpoint endpoint;
-    private CoapClient client;
+    // set by initClient under the lock, and set to null by doStop, which 
BaseService.stop runs under the same lock;
+    // volatile for the readers that do not take the lock (getClient)
+    private volatile CoapClient client;
     private boolean shutdownClient;
 
     public CoAPProducer(CoAPEndpoint endpoint) {
@@ -50,7 +53,16 @@ public class CoAPProducer extends DefaultProducer {
 
     @Override
     public void process(Exchange exchange) throws Exception {
-        initClient();
+        // doStop sets the client field to null, possibly while this request 
is in flight: take the client once, under
+        // the lock that doStop holds, so it cannot become null between 
initClient and this read
+        CoapClient coapClient;
+        lock.lock();
+        try {
+            initClient();
+            coapClient = client;
+        } finally {
+            lock.unlock();
+        }
 
         String ct = exchange.getIn().getHeader(CoAPConstants.CONTENT_TYPE, 
String.class);
         if (ct == null) {
@@ -58,27 +70,27 @@ public class CoAPProducer extends DefaultProducer {
             ct = "application/octet-stream";
         }
         // the method header may be given in lower case
-        String method = CoAPHelper.getDefaultMethod(exchange, 
client).toUpperCase(Locale.ROOT);
+        String method = CoAPHelper.getDefaultMethod(exchange, 
coapClient).toUpperCase(Locale.ROOT);
         int mediaType = MediaTypeRegistry.parse(ct);
         CoapResponse response = null;
         boolean pingResponse = false;
         switch (method) {
             case CoAPConstants.METHOD_GET:
-                response = client.get();
+                response = coapClient.get();
                 break;
             case CoAPConstants.METHOD_DELETE:
-                response = client.delete();
+                response = coapClient.delete();
                 break;
             case CoAPConstants.METHOD_POST:
                 byte[] bodyPost = exchange.getIn().getBody(byte[].class);
-                response = client.post(bodyPost, mediaType);
+                response = coapClient.post(bodyPost, mediaType);
                 break;
             case CoAPConstants.METHOD_PUT:
                 byte[] bodyPut = exchange.getIn().getBody(byte[].class);
-                response = client.put(bodyPut, mediaType);
+                response = coapClient.put(bodyPut, mediaType);
                 break;
             case CoAPConstants.METHOD_PING:
-                pingResponse = client.ping();
+                pingResponse = coapClient.ping();
                 break;
             default:
                 throw new IllegalArgumentException("Unsupported CoAP method: " 
+ method);
@@ -89,7 +101,7 @@ public class CoAPProducer extends DefaultProducer {
         } else if (!method.equals(CoAPConstants.METHOD_PING)) {
             // the client returns null when no response was received (timeout, 
rejected or cancelled request)
             throw new CamelExchangeException(
-                    "No response received from CoAP server for " + method + ": 
" + URISupport.sanitizeUri(client.getURI()),
+                    "No response received from CoAP server for " + method + ": 
" + URISupport.sanitizeUri(coapClient.getURI()),
                     exchange);
         }
 
@@ -114,7 +126,13 @@ public class CoAPProducer extends DefaultProducer {
     @Override
     protected void doStop() throws Exception {
         if (shutdownClient && client != null) {
+            // the client was created by this producer with its own endpoint 
(createCoapClient), which
+            // CoapClient.shutdown() does not release (bound socket and 
connector threads)
+            Endpoint clientEndpoint = client.getEndpoint();
             client.shutdown();
+            if (clientEndpoint != null) {
+                clientEndpoint.destroy();
+            }
             client = null;
         }
     }
diff --git 
a/components/camel-coap/src/test/java/org/apache/camel/coap/CoAPComponentStopDestroysServersTest.java
 
b/components/camel-coap/src/test/java/org/apache/camel/coap/CoAPComponentStopDestroysServersTest.java
new file mode 100644
index 000000000000..663001f914a2
--- /dev/null
+++ 
b/components/camel-coap/src/test/java/org/apache/camel/coap/CoAPComponentStopDestroysServersTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.coap;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.test.AvailablePortFinder;
+import org.eclipse.californium.core.CoapClient;
+import org.eclipse.californium.core.CoapResponse;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Stopping the CamelContext must release the CoAP servers of the component: 
CoapServer.stop() keeps the executors of
+ * the server, whose threads are not daemon threads, so a server that is only 
stopped (not destroyed) keeps the JVM
+ * running after the CamelContext is stopped.
+ */
+class CoAPComponentStopDestroysServersTest {
+
+    @RegisterExtension
+    static AvailablePortFinder.Port port = CoAPTestPorts.findUdpPort();
+
+    @Test
+    void stoppingTheCamelContextReleasesTheServerThreads() throws Exception {
+        Set<Thread> before = Thread.getAllStackTraces().keySet();
+        CamelContext context = createContext();
+        try {
+            context.start();
+            assertEquals("Hi", get());
+        } finally {
+            context.stop();
+        }
+
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            List<String> left = Thread.getAllStackTraces().keySet().stream()
+                    .filter(t -> !before.contains(t) && t.isAlive() && 
!t.isDaemon()
+                            && t.getName().startsWith("CoapServer"))
+                    .map(Thread::getName).sorted().toList();
+            assertEquals(List.of(), left, "non-daemon CoapServer threads still 
alive after CamelContext.stop()");
+        });
+    }
+
+    @Test
+    void aCamelContextStartedAgainServesAgain() throws Exception {
+        // control: the servers destroyed by the stop are created again by the 
consumers when the context starts again
+        CamelContext context = createContext();
+        try {
+            context.start();
+            assertEquals("Hi", get());
+            context.stop();
+            context.start();
+            assertEquals("Hi", get());
+        } finally {
+            context.stop();
+        }
+    }
+
+    private static CamelContext createContext() throws Exception {
+        CamelContext context = new DefaultCamelContext();
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                fromF("coap://localhost:%d/hello", 
port.getPort()).setBody(constant("Hi"));
+            }
+        });
+        return context;
+    }
+
+    private static String get() throws Exception {
+        CoapClient client = new CoapClient("coap://localhost:" + 
port.getPort() + "/hello");
+        try {
+            CoapResponse response = client.get();
+            assertNotNull(response, "no response from the CoAP server");
+            return response.getResponseText();
+        } finally {
+            client.shutdown();
+        }
+    }
+}
diff --git 
a/components/camel-coap/src/test/java/org/apache/camel/coap/CoAPProducerNoResponseTest.java
 
b/components/camel-coap/src/test/java/org/apache/camel/coap/CoAPProducerNoResponseTest.java
index 408e1058645f..666aa6aa4824 100644
--- 
a/components/camel-coap/src/test/java/org/apache/camel/coap/CoAPProducerNoResponseTest.java
+++ 
b/components/camel-coap/src/test/java/org/apache/camel/coap/CoAPProducerNoResponseTest.java
@@ -16,6 +16,14 @@
  */
 package org.apache.camel.coap;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
 import org.apache.camel.BindToRegistry;
 import org.apache.camel.CamelExchangeException;
 import org.apache.camel.Exchange;
@@ -23,11 +31,15 @@ import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
 import org.apache.camel.test.AvailablePortFinder;
 import org.eclipse.californium.core.CoapClient;
+import org.eclipse.californium.core.coap.MessageObserverAdapter;
+import org.eclipse.californium.core.coap.Request;
+import 
org.eclipse.californium.core.network.interceptors.MessageInterceptorAdapter;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -61,6 +73,55 @@ public class CoAPProducerNoResponseTest extends 
CoAPTestSupport {
         MockEndpoint.assertIsSatisfied(context);
     }
 
+    @Test
+    void testStoppingTheProducerWithARequestInFlightFailsTheExchange() throws 
Exception {
+        CoAPEndpoint endpoint = context.getEndpoint(
+                String.format("coap://localhost:%d/TestResource", 
unusedPort.getPort()), CoAPEndpoint.class);
+        CoAPProducer producer = (CoAPProducer) endpoint.createProducer();
+        producer.start();
+        producer.initClient();
+
+        AtomicReference<Request> sentRequest = new AtomicReference<>();
+        CountDownLatch requestSent = new CountDownLatch(1);
+        producer.getClient().getEndpoint().addInterceptor(new 
MessageInterceptorAdapter() {
+            @Override
+            public void sendRequest(Request request) {
+                // called before the connector sends the datagram
+                request.addMessageObserver(new MessageObserverAdapter() {
+                    @Override
+                    public void onSent(boolean retransmission) {
+                        sentRequest.set(request);
+                        requestSent.countDown();
+                    }
+                });
+            }
+        });
+
+        Exchange exchange = endpoint.createExchange();
+        exchange.getIn().setHeader(CoAPConstants.COAP_METHOD, 
CoAPConstants.METHOD_GET);
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        try {
+            Future<?> inflight = executor.submit(() -> {
+                producer.process(exchange);
+                return null;
+            });
+            assertTrue(requestSent.await(5, TimeUnit.SECONDS), "the request 
was not sent");
+
+            // destroying the client endpoint cancels the request; the 
requesting thread waits for the response on the
+            // monitor of the request, so holding it here lets that thread go 
on only after doStop has completed
+            Request request = sentRequest.get();
+            synchronized (request) {
+                producer.stop();
+            }
+
+            ExecutionException failure = 
assertThrows(ExecutionException.class, () -> inflight.get(5, TimeUnit.SECONDS));
+            CamelExchangeException cause = 
assertInstanceOf(CamelExchangeException.class, failure.getCause());
+            assertTrue(cause.getMessage().startsWith("No response received 
from CoAP server for GET"), cause.getMessage());
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
     @Override
     protected RouteBuilder createRouteBuilder() {
         return new RouteBuilder() {
diff --git 
a/components/camel-coap/src/test/java/org/apache/camel/coap/CoAPProducerStopDestroysEndpointTest.java
 
b/components/camel-coap/src/test/java/org/apache/camel/coap/CoAPProducerStopDestroysEndpointTest.java
new file mode 100644
index 000000000000..193133789dbd
--- /dev/null
+++ 
b/components/camel-coap/src/test/java/org/apache/camel/coap/CoAPProducerStopDestroysEndpointTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.coap;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+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;
+
+/**
+ * Each start of a CoAP producer creates a CoapClient with its own 
CoapEndpoint (a bound UDP socket and its receiver and
+ * sender threads). CoapClient.shutdown() does not destroy that endpoint, so 
the producer must do it when it stops.
+ */
+class CoAPProducerStopDestroysEndpointTest extends CoAPTestSupport {
+
+    @Test
+    void routeRestartsReleaseTheEndpointsOfTheStoppedProducers() throws 
Exception {
+        assertEquals("Hello Camel", template.requestBody("direct:start", 
"Camel", String.class));
+        Set<Thread> before = Thread.getAllStackTraces().keySet();
+        for (int i = 0; i < 5; i++) {
+            context.getRouteController().stopRoute("producer");
+            context.getRouteController().startRoute("producer");
+            assertEquals("Hello Camel", template.requestBody("direct:start", 
"Camel", String.class));
+        }
+        context.getRouteController().stopRoute("producer");
+
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            long receivers = Thread.getAllStackTraces().keySet().stream()
+                    .filter(t -> !before.contains(t) && t.isAlive() && 
t.getName().startsWith("UDP-Receiver"))
+                    .count();
+            assertEquals(0, receivers, "UDP receiver threads of stopped CoAP 
producers still alive after 5 restarts");
+        });
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                fromF("coap://localhost:%d/hello", 
PORT.getPort()).transform(simple("Hello ${body}"));
+                
from("direct:start").routeId("producer").toF("coap://localhost:%d/hello", 
PORT.getPort());
+            }
+        };
+    }
+}

Reply via email to