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