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 74c573d323c6 CAMEL-26570: camel-milo - deliver the loss report of a 
consumer started without a connection off the request pool (#29197)
74c573d323c6 is described below

commit 74c573d323c6c42ce3511c3152d9600e00eb7366
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Nov 5 12:34:19 2026 +0530

    CAMEL-26570: camel-milo - deliver the loss report of a consumer started 
without a connection off the request pool (#29197)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../milo/client/internal/SubscriptionManager.java  |  6 ++-
 .../component/milo/CommunicationLossTest.java      | 62 ++++++++++++++++++++++
 2 files changed, 66 insertions(+), 2 deletions(-)

diff --git 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
index f62ec04dee41..4c7d45fe8385 100644
--- 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
+++ 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
@@ -66,6 +66,7 @@ import 
org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaMonitoredItem;
 import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscription;
 import org.eclipse.milo.opcua.stack.core.AttributeId;
 import org.eclipse.milo.opcua.stack.core.NodeIds;
+import org.eclipse.milo.opcua.stack.core.Stack;
 import org.eclipse.milo.opcua.stack.core.StatusCodes;
 import org.eclipse.milo.opcua.stack.core.UaException;
 import org.eclipse.milo.opcua.stack.core.security.CertificateValidator;
@@ -1710,9 +1711,10 @@ public class SubscriptionManager {
             if (this.connected == null) {
                 // no communication with the server: the nodes are reported 
off the thread that starts the consumer,
                 // with a generation below every connection's, so the report 
is dropped once any connection has
-                // delivered their values, also one that was being built while 
they were registered
+                // delivered their values, also one that was being built while 
they were registered. Not on the request
+                // pool: the route may wait for a request of this connection, 
whose outcome needs a thread of that pool
                 final DateTime time = DateTime.now();
-                this.executor.execute(() -> deliverLoss(subscriptions, 0L, 
time));
+                Stack.sharedExecutor().execute(() -> 
deliverLoss(subscriptions, 0L, time));
             } else if (!this.connected.listener.sessionActive) {
                 // the session is lost; the nodes are reported before the 
values of their items, on the same queue
                 reportLoss(this.connected.manager, this.connected.generation, 
subscriptions);
diff --git 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/CommunicationLossTest.java
 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/CommunicationLossTest.java
index 48d16171fff9..467f5543d90a 100644
--- 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/CommunicationLossTest.java
+++ 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/CommunicationLossTest.java
@@ -26,6 +26,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.camel.EndpointInject;
@@ -45,11 +46,14 @@ import org.eclipse.milo.opcua.stack.core.StatusCodes;
 import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
 import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
 import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
+import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
+import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import static org.awaitility.Awaitility.await;
+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.assertNotNull;
@@ -113,6 +117,9 @@ public class CommunicationLossTest extends 
AbstractMiloServerTest {
     @RegisterExtension
     AvailablePortFinder.Port disposedRelayPort = AvailablePortFinder.find();
 
+    @RegisterExtension
+    AvailablePortFinder.Port reportRelayPort = AvailablePortFinder.find();
+
     private final TcpRelay relay = new TcpRelay();
 
     private final TcpRelay lateRelay = new TcpRelay();
@@ -121,6 +128,8 @@ public class CommunicationLossTest extends 
AbstractMiloServerTest {
 
     private final TcpRelay disposedRelay = new TcpRelay();
 
+    private final TcpRelay reportRelay = new TcpRelay();
+
     @Override
     protected RoutesBuilder createRouteBuilder() {
         return new RouteBuilder() {
@@ -166,6 +175,7 @@ public class CommunicationLossTest extends 
AbstractMiloServerTest {
         lateRelay.stop();
         reconnectTimeoutRelay.stop();
         disposedRelay.stop();
+        reportRelay.stop();
     }
 
     @Test
@@ -391,6 +401,58 @@ public class CommunicationLossTest extends 
AbstractMiloServerTest {
         }
     }
 
+    /**
+     * The report of a consumer that starts while the server cannot be reached 
runs its route, which can wait for a
+     * request of the same connection, here a write once the connection is up, 
as a redelivery does. The outcome of the
+     * request does not wait for the thread that delivers the report, also 
when the pool that completes the requests has
+     * a single thread, as on a machine with one processor.
+     */
+    @Test
+    @Timeout(value = 60, unit = TimeUnit.SECONDS)
+    void consumerThatWaitsForAWriteOnItsLossReportCompletesWithOneThread() 
throws Exception {
+        producer1.sendBody("Foo");
+
+        final MiloClientEndpoint endpoint = context.getEndpoint(
+                resolve(MILO_CLIENT_ITEM_1).replace("@@relay@@", 
Integer.toString(reportRelayPort.getPort())),
+                MiloClientEndpoint.class);
+        final ScheduledExecutorService reconnectExecutor = 
Executors.newSingleThreadScheduledExecutor();
+        final ExecutorService executor = Executors.newSingleThreadExecutor();
+        // the relay is down: the manager cannot connect
+        final SubscriptionManager manager
+                = new SubscriptionManager(endpoint.getConfiguration(), 
reconnectExecutor, executor, 200);
+        final CountDownLatch connected = new CountDownLatch(1);
+        final CompletableFuture<Object> write = new CompletableFuture<>();
+        try {
+            final ExpandedNodeId item = 
ExpandedNodeId.parse(endpoint.getNode());
+            manager.registerItems(Map.of(item, value -> {
+                if (value.getStatusCode().getValue() != 
StatusCodes.Bad_NoCommunication || write.isDone()) {
+                    return;
+                }
+                try {
+                    if (!connected.await(30, TimeUnit.SECONDS)) {
+                        throw new TimeoutException("The connection did not 
come up");
+                    }
+                    write.complete(manager.write(item, new DataValue(new 
Variant("Bar"))).get(10, TimeUnit.SECONDS));
+                } catch (final Exception e) {
+                    write.completeExceptionally(e);
+                }
+            }), 50.0, null, new MonitorFilterConfiguration());
+
+            reportRelay.start(reportRelayPort.getPort(), getServerPort());
+            await().atMost(30, TimeUnit.SECONDS).until(() -> 
manager.getOpcUaClient() != null);
+            connected.countDown();
+
+            final Object status = assertDoesNotThrow(() -> write.get(30, 
TimeUnit.SECONDS),
+                    "The write of the consumer did not complete: its outcome 
waits for the thread that delivers the report");
+            assertEquals(StatusCode.GOOD, status);
+        } finally {
+            connected.countDown();
+            manager.dispose();
+            reconnectExecutor.shutdownNow();
+            executor.shutdownNow();
+        }
+    }
+
     /**
      * A connection that the manager completes after it has been disposed is 
closed: the manager no longer holds it, so
      * nothing else would close its session.

Reply via email to