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 99af30362c81 CAMEL-25671: camel-milo - report a node with an unknown 
namespace URI instead of failing the connection (#27906)
99af30362c81 is described below

commit 99af30362c815036c93b7a850268b7c81c5f3066
Author: Serdar Gökay <[email protected]>
AuthorDate: Sat Oct 17 17:31:52 2026 +0300

    CAMEL-25671: camel-milo - report a node with an unknown namespace URI 
instead of failing the connection (#27906)
    
    A consumer node ID whose namespace URI (nsu=) is not in the namespace
    table of the server made lookupNamespace throw a NullPointerException.
    The exception went to the reconnect path, so one wrong entry in a nodes
    list took the whole client connection down, including the consumers of
    the other nodes.
    
    The consumer now treats such a node like a node that does not exist on
    the server: one WARN with the node ID and one DataValue with
    Bad_NodeIdUnknown for that node (visible with omitNullValues=false); the
    other nodes and the connection keep working.
    
    The badSubscriptions map and the 10 second resubscribe that served the
    old Bad_InvalidArgument branch are removed. That branch could not be
    reached, because lookupNamespace never returned null.
    
    Producers (write, call, read, browse) are not changed.
    
    Co-authored-by: Claude Fable 5.1 <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../camel/catalog/docs/milo-client-component.adoc  |  1 +
 .../src/main/docs/milo-client-component.adoc       |  1 +
 .../milo/client/internal/SubscriptionManager.java  | 40 ++++++++--------------
 .../camel/component/milo/MonitorNodesTest.java     | 35 +++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  4 +++
 5 files changed, 55 insertions(+), 26 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
index 141594571be5..bf6f7bd1252b 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
@@ -149,6 +149,7 @@ need a consumer of their own.
 
 A node that does not exist on the server does not affect the other nodes. A 
node that cannot be monitored is
 reported once with its status code and is not retried; restarting the route or 
a rebuilt connection to the server creates the monitored items again.
+A node whose namespace URI (`nsu=`) is not in the namespace table of the 
server is reported in the same way.
 The consumer delivers one exchange with the bad status code for such a node, 
and the `CamelMiloNodeId` header names it.
 
 === Status codes and null values
diff --git a/components/camel-milo/src/main/docs/milo-client-component.adoc 
b/components/camel-milo/src/main/docs/milo-client-component.adoc
index 141594571be5..bf6f7bd1252b 100644
--- a/components/camel-milo/src/main/docs/milo-client-component.adoc
+++ b/components/camel-milo/src/main/docs/milo-client-component.adoc
@@ -149,6 +149,7 @@ need a consumer of their own.
 
 A node that does not exist on the server does not affect the other nodes. A 
node that cannot be monitored is
 reported once with its status code and is not retried; restarting the route or 
a rebuilt connection to the server creates the monitored items again.
+A node whose namespace URI (`nsu=`) is not in the namespace table of the 
server is reported in the same way.
 The consumer delivers one exchange with the bad status code for such a node, 
and the `CamelMiloNodeId` header names it.
 
 === Status codes and null values
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 bacf42dcf0a9..ed096e646d49 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
@@ -177,8 +177,6 @@ public class SubscriptionManager {
         private OpcUaClient client;
         private final OpcUaSubscription manager;
 
-        private final Map<UInteger, Subscription> badSubscriptions = new 
HashMap<>();
-
         private final Map<UInteger, OpcUaMonitoredItem> goodSubscriptions = 
new HashMap<>();
 
         private final Map<String, UShort> namespaceCache = new 
ConcurrentHashMap<>();
@@ -214,10 +212,14 @@ public class SubscriptionManager {
             for (final Map.Entry<UInteger, Subscription> entry : 
subscriptions.entrySet()) {
                 final Subscription s = entry.getValue();
 
-                final NodeId node = lookupNamespace(s.getNodeId()).get();
+                final NodeId node = lookupMonitoredNode(s.getNodeId());
 
                 if (node == null) {
-                    handleSubscriptionError(new 
StatusCode(StatusCodes.Bad_InvalidArgument), entry.getKey(), s, pendingValues);
+                    // reported once like a node the server rejects, the other 
nodes and the connection are not affected
+                    final StatusCode status = new 
StatusCode(StatusCodes.Bad_NodeIdUnknown);
+                    LOG.warn("Monitored item for node {} could not be created, 
the namespace URI is unknown to the server: {}",
+                            s.getNodeId().toParseableString(), status);
+                    pendingValues.add(new PendingValue(s.getValueConsumer(), 
new DataValue(status)));
                 } else {
                     final ReadValueId itemId = new ReadValueId(node, 
AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE);
                     final OpcUaMonitoredItem item = new 
OpcUaMonitoredItem(itemId, MonitoringMode.Reporting);
@@ -283,29 +285,17 @@ public class SubscriptionManager {
                 }
             }
 
-            if (!this.badSubscriptions.isEmpty()) {
-                SubscriptionManager.this.executor.schedule(this::resubscribe, 
SubscriptionManager.this.reconnectTimeout,
-                        TimeUnit.MILLISECONDS);
-            }
-
             return pendingValues;
         }
 
-        private void handleSubscriptionError(
-                final StatusCode statusCode, final UInteger clientHandle, 
final Subscription s,
-                final List<PendingValue> pendingValues) {
-            this.badSubscriptions.put(clientHandle, s);
-            pendingValues.add(new PendingValue(s.getValueConsumer(), new 
DataValue(statusCode)));
-        }
-
-        private void resubscribe() {
-            final Map<UInteger, Subscription> subscriptions = new 
HashMap<>(this.badSubscriptions);
-            this.badSubscriptions.clear();
-            try {
-                putSubscriptions(subscriptions).forEach(PendingValue::deliver);
-            } catch (final Exception e) {
-                handleConnectionFailure(e);
-            }
+        /**
+         * @return the node ID with the namespace index of the server, or null 
if its namespace URI is not in the
+         *         namespace table of the server
+         */
+        private NodeId lookupMonitoredNode(final ExpandedNodeId nodeId) throws 
Exception {
+            final String uri = nodeId.getNamespaceUri();
+            final UShort index = uri == null ? nodeId.getNamespaceIndex() : 
lookupNamespaceIndex(uri).get();
+            return index == null ? null : toNodeId(index, nodeId);
         }
 
         public void deactivate(final List<UInteger> clientHandles) throws 
Exception {
@@ -314,8 +304,6 @@ public class SubscriptionManager {
                 final OpcUaMonitoredItem item = 
this.goodSubscriptions.remove(clientHandle);
                 if (item != null) {
                     items.add(item);
-                } else {
-                    this.badSubscriptions.remove(clientHandle);
                 }
             }
             if (!items.isEmpty()) {
diff --git 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorNodesTest.java
 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorNodesTest.java
index cef04fa5f63c..dff10d98fe7c 100644
--- 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorNodesTest.java
+++ 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorNodesTest.java
@@ -52,6 +52,9 @@ public class MonitorNodesTest extends AbstractMiloServerTest {
 
     private static final String UNKNOWN_NODE = "nsu=" + 
MiloServerComponent.DEFAULT_NAMESPACE_URI + ";s=unknownNode";
 
+    // a namespace URI that is not in the namespace table of the server
+    private static final String UNKNOWN_NAMESPACE_NODE = 
"nsu=urn:not:there;s=node1";
+
     // the same node as NODE_1 with the default server index spelled out
     private static final String NODE_1_WITH_SERVER_INDEX = "svr=0;" + NODE_1;
 
@@ -65,6 +68,9 @@ public class MonitorNodesTest extends AbstractMiloServerTest {
     private static final String MILO_CLIENT_UNKNOWN_NODE
             = MILO_CLIENT + "&nodes=RAW(" + NODE_1 + "," + UNKNOWN_NODE + "," 
+ NODE_2 + ")";
 
+    private static final String MILO_CLIENT_UNKNOWN_NAMESPACE
+            = MILO_CLIENT + "&nodes=RAW(" + NODE_1 + "," + 
UNKNOWN_NAMESPACE_NODE + "," + NODE_2 + ")";
+
     private static final String MILO_CLIENT_DUPLICATES
             = MILO_CLIENT + "&nodes=RAW( " + NODE_1 + " ,, " + NODE_2 + " ," + 
NODE_1 + ")";
 
@@ -79,6 +85,9 @@ public class MonitorNodesTest extends AbstractMiloServerTest {
     @EndpointInject("mock:unknownNode")
     protected MockEndpoint unknownNodeEndpoint;
 
+    @EndpointInject("mock:unknownNamespace")
+    protected MockEndpoint unknownNamespaceEndpoint;
+
     @EndpointInject("mock:duplicates")
     protected MockEndpoint duplicatesEndpoint;
 
@@ -96,6 +105,8 @@ public class MonitorNodesTest extends AbstractMiloServerTest 
{
                 // the consumers deliver the current values while they start, 
so each test starts its route
                 
from(resolve(MILO_CLIENT_NODES)).routeId("nodes").autoStartup(false).to("mock:nodes");
                 
from(resolve(MILO_CLIENT_UNKNOWN_NODE)).routeId("unknownNode").autoStartup(false).to("mock:unknownNode");
+                
from(resolve(MILO_CLIENT_UNKNOWN_NAMESPACE)).routeId("unknownNamespace").autoStartup(false)
+                        .to("mock:unknownNamespace");
                 
from(resolve(MILO_CLIENT_DUPLICATES)).routeId("duplicates").autoStartup(false).to("mock:duplicates");
                 
from(resolve(MILO_CLIENT_NODE)).routeId("node").autoStartup(false).to("mock:node");
                 
from(resolve(MILO_CLIENT_SAME_NODE)).routeId("sameNode").autoStartup(false).to("mock:sameNode");
@@ -171,6 +182,30 @@ public class MonitorNodesTest extends 
AbstractMiloServerTest {
         unknownNodeEndpoint.assertIsSatisfied();
     }
 
+    @Test
+    void unknownNamespaceUriReceivesBadStatusAndTheOtherNodesKeepWorking() 
throws Exception {
+        writeServerValues();
+
+        // the bad status is reported once, the connection stays up for the 
other nodes
+        unknownNamespaceEndpoint.expectedMessageCount(3);
+        unknownNamespaceEndpoint.setAssertPeriod(1000);
+        context.getRouteController().startRoute("unknownNamespace");
+        unknownNamespaceEndpoint.assertIsSatisfied();
+
+        final Map<String, DataValue> values = 
valuesByNode(unknownNamespaceEndpoint);
+        assertEquals(Set.of(NODE_1, UNKNOWN_NAMESPACE_NODE, NODE_2), 
values.keySet());
+        assertEquals(new StatusCode(StatusCodes.Bad_NodeIdUnknown), 
values.get(UNKNOWN_NAMESPACE_NODE).getStatusCode());
+        assertGoodValue("Foo").accept(values.get(NODE_1));
+        assertGoodValue("Bar").accept(values.get(NODE_2));
+
+        unknownNamespaceEndpoint.reset();
+        unknownNamespaceEndpoint.expectedMessageCount(1);
+        
unknownNamespaceEndpoint.expectedHeaderReceived(MiloConstants.HEADER_NODE_ID, 
NODE_1);
+        testBody(unknownNamespaceEndpoint.message(0), assertGoodValue("Baz"));
+        template.sendBody("milo-server:node1", "Baz");
+        unknownNamespaceEndpoint.assertIsSatisfied();
+    }
+
     @Test
     void duplicateNodesAreMonitoredOnce() throws Exception {
         writeServerValues();
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index cbb1be435b41..1e5463becddd 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1534,6 +1534,10 @@ exist, received nothing. Now it receives one `DataValue` 
with the bad status cod
 monitored item is created again when the route is restarted or when Camel 
rebuilds the connection to the server; unlike
 Camel 3.x, it is not retried every 10 seconds.
 
+A node ID with a namespace URI (`nsu=`) that is not in the namespace table of 
the server no longer takes the whole
+client connection down, including the consumers of other nodes. Its consumer 
receives one `DataValue` with
+`Bad_NodeIdUnknown`, like a node that does not exist.
+
 The consumer now sets the `CamelMiloNodeId` header on every exchange, 
including a consumer with a single `node`.
 
 Stopping a consumer now deletes its monitored items. Previously they stayed on 
the server while another endpoint kept

Reply via email to