Skip to content

CAMEL-25690: camel-milo - answer a producer node with an unknown namespace URI instead of reconnecting - #28045

Merged
davsclaus merged 1 commit into
apache:mainfrom
etzel-mes:fix/CAMEL-25690-milo-producer-unknown-namespace
Oct 19, 2026
Merged

davsclaus merged 1 commit into
apache:mainfrom
etzel-mes:fix/CAMEL-25690-milo-producer-unknown-namespace

Conversation

@etzel-mes

@etzel-mes etzel-mes commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Description

CAMEL-25690.

The milo-client producer (write, method call, read) and the milo-browse producer resolve the namespace URI (nsu=) of a node ID through the namespace table of the server. When the URI was not in the table, the lookup failed with a NullPointerException, and the producer took it for a lost connection: the client reconnected, which also recreated the subscriptions of the consumers that share the connection, and a read lost the values of its other nodes.

Why this matters in automation: a namespace URI is configuration, and it goes stale when a server is reconfigured or replaced. A route with a stale URI then reconnected the client on every request. I checked this against a server that is not built on Milo (Microsoft opc-plc 2.15.4, on the OPC Foundation .NET stack). Without this change, one awaited write to such a node replaced the OpcUaClient of the connection, and the requests sent right after it failed with No connected: a write and two method calls that the server would have answered itself. One such browse reconnected the browse connection.

The change:

  • The consumer and the producers now share one lookup, as suggested in the review of CAMEL-25671 (CAMEL-25671: camel-milo - report a node with an unknown namespace URI instead of failing the connection #27906). lookupNamespace resolves the namespace URI or index, completes with null for a URI that the server does not know, and replaces the consumer's lookupMonitoredNode. The consumer reports such a node with Bad_NodeIdUnknown and resolves it under the lock, both as before; its lookup still blocks while the monitored items are created, as before.
  • A producer answers such a node the way a server answers a node that it does not have (OPC 10000-4 v1.05, Table 179, and Table 61 for the Call service), without sending it to the server: a write returns Bad_NodeIdUnknown; a read returns Bad_NodeIdUnknown for that node in its position, and a browse under that node ID, both with the results of the other nodes; a method call returns Bad_NodeIdUnknown for an unknown object and Bad_MethodInvalid for an unknown method. A request without such a node takes the same path as before. A read or browse of only such nodes sends no request, because a server answers an empty one with Bad_NothingToDo, which the producer takes for a lost connection. A DEBUG line names the node, because a write that does not wait for its result reports nothing else.
  • New ProducerUnknownNamespaceTest: a write, and reads and browses of such a node next to a known node and alone, get these results, and a method call with an unknown method or object completes without an error. Each connection keeps its OpcUaClient, and after the write, the reads and the calls a consumer on the same connection receives the next value. Without the fix all four tests fail, and the client reconnects in each.
  • Component docs (Node ID section, with its catalog copy) and a 4.24 upgrade-guide note.

Against opc-plc with the change, each answer matched the answer of the server for a node it does not have (ns=99;s=…, or an unknown identifier in a known namespace): one read of six nodes returned Good, Good, Bad_NodeIdUnknown, Good, Bad_NodeIdUnknown, Bad_NodeIdUnknown in the requested order, and a browse returned the references of Objects next to Bad_NodeIdUnknown. Both connections kept their OpcUaClient, and a consumer on the client connection kept receiving its counter. On top of #27956, the two method calls also returned what the server returns for an unknown method of an object (Bad_MethodInvalid) and for an unknown object (Bad_NodeIdUnknown).

This PR does not change the four request guards in SubscriptionManager that #27956 (CAMEL-25694, @gnodet) changes, and the two merge without conflicts in either order. The call guard on main drops every method call result, so for now a method call with such a node only keeps the connection, and its answer reaches the exchange with #27956. The docs and the test therefore leave the call answer out; I can add it to both once #27956 is merged. On top of #27956, such call assertions and the tests of #27956 already pass.

The unchecked lookup dates back to the first client implementation and came back with the component in 84b7e90; this change does not revert any later work.

Checked with -Psourcecheck clean verify on the module (83 tests), four more runs of the new and the affected test classes with -Dsurefire.rerunFailingTestsCount=0, one of them with -XX:ActiveProcessorCount=1, and the new test together with the tests of #27956 on top of it.

@davsclaus @oscerd could you take a look?

Target

  • I checked that the commit is targeting the correct branch (Camel 4 uses the main branch)

Tracking

  • If this is a large change, bug fix, or code improvement, I checked there is a JIRA issue filed for the change (usually before you start working on it).

Apache Camel coding standards and style

  • I checked that each commit in the pull request has a meaningful subject line and body.

  • I have run mvn clean install -DskipTests locally from root folder and I have committed all auto-generated changes.

AI-assisted contributions

  • If this PR includes AI-generated code, commits have proper co-authorship attribution (e.g., Co-authored-by trailers) and the PR description identifies the AI tool used.

Claude Code on behalf of etzel-mes

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-structured fix for CAMEL-25690. The root cause was correctly identified: lookupNamespace() could pass a null index to toNodeId(), which caused an NPE completing the future exceptionally and triggering the reconnect logic.

All changes are correct:

  • lookupNamespace() now returns null for unknown namespace URIs instead of NPE-triggering the future
  • write(), call(), readValues(), and browse() all intercept null NodeId and return the appropriate OPC UA status codes locally — no server round-trip needed
  • readValues() iterator alignment is correct: known is built in the same order as nodeIds, so the per-element reconstruction is safe
  • known.isEmpty() guards correctly avoid empty server requests (which would return Bad_NothingToDo per spec)
  • putSubscriptions() refactor is functionally equivalent — old lookupMonitoredNode() did the same synchronous null-guarded lookup
  • Tests cover all four fixed code paths with mixed and unknown-only scenarios, and verify that the connection is preserved
  • Upgrade guide accurately describes behavior before and after

✅ APPROVE

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@gnodet gnodet added the lts label Oct 18, 2026
…space URI instead of reconnecting

The producers resolved the namespace URI (nsu=) of a node ID through
the namespace table of the server and failed with a
NullPointerException when the URI was not in it. The write, method
call, read or browse took that for a lost connection: the client
reconnected, which also recreated the subscriptions of the consumers
that share the connection.

The consumer and the producers now share one node lookup, which
completes with null for such a URI, as the lookup of the consumer has
done since CAMEL-25671. A producer answers such a node like a node the
server does not have, without sending it: Bad_NodeIdUnknown for a
write and for that node of a read or browse, and Bad_NodeIdUnknown or
Bad_MethodInvalid for a method call. The other nodes of the request,
the connection and its consumers are not affected. The method call
answer does not reach the exchange yet, because
SubscriptionManager.call drops every call result (CAMEL-25694).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@etzel-mes
etzel-mes force-pushed the fix/CAMEL-25690-milo-producer-unknown-namespace branch from 0fbe4c9 to ec1e6a7 Compare October 18, 2026 08:10
@etzel-mes

Copy link
Copy Markdown
Contributor Author

Rebased onto main after #27910 was merged. The conflicts were only textual. The lookupMonitoredNode that this PR replaces with the shared lookup sat next to the new recreate; recreate calls putSubscriptions, so it now uses the shared lookup too. The two camel-milo notes in the 4.24 upgrade guide were next to each other. Otherwise the change is the same: -Psourcecheck clean verify on the module passes with 83 tests, and the branch still merges with #27956 without conflicts.

Claude Code on behalf of etzel-mes

@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @etzel-mes. I checked each producer path against main. There, toNodeId throws a NullPointerException for the missing index, and the handleAsync guard turns it into handleConnectionFailure and returns null. So an awaited write or read completed with a null body, and the browse producer failed with an NPE on browseResults.values(). The upgrade-guide note describes this correctly.

With the change, the lookup completes with null, and the write, call, read and browse answer such a node without asking the server. A mixed read keeps the requested order, because the known values are taken in request order. A read or browse of only unknown nodes sends no request. The local answers go through the same guard as real server answers, so they will get whatever handling #27956 adds. The consumer behaves as before: lookupNamespace(...).get() does the same as the old lookupMonitoredNode.

The component doc and its catalog copy are identical. The new test follows the JUnit style of the module and has no sleeps. The current head is a rebase of the commit gnodet-bot reviewed (the range-diff shows only context changes), and it merges cleanly with #28049 and #27956.

One nit inline. Approving.

Claude Code on behalf of Claus Ibsen

@github-actions

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • catalog/camel-catalog
  • components/camel-milo
  • docs

🔬 Scalpel shadow comparison — Scalpel: 8 of 706 tested, 25 compile-only — current: 8 all tested

Maveniverse Scalpel detected 8 affected modules (current approach: 8).

Skip-tests mode would test 8 modules (3 direct + 7 downstream), skip tests for 25 (generated code, meta-modules)

Modules Scalpel would test (8)
  • camel-jbang-mcp ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-mcp ← downstream of org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-tui ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-validate ← downstream of org.apache.camel:camel-yaml-dsl-validator
  • camel-launcher-container ← downstream of org.apache.camel:camel-launcher
  • camel-milo ← components/camel-milo/src/main/docs/milo-client-component.adoc, components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java, components/camel-milo/src/test/java/org/apache/camel/component/milo/ProducerUnknownNamespaceTest.java
  • camel-yaml-dsl-validator ← downstream of org.apache.camel:camel-catalog
  • camel-yaml-dsl-validator-maven-plugin ← downstream of org.apache.camel:camel-yaml-dsl-validator
Modules with tests skipped (25)
  • apache-camel
  • camel-allcomponents
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

All tested modules (35 modules, 4m 46s total)

Total reactor time: 4m 46s

Module Duration Status
Camel :: Launcher 41.6s SUCCESS
Camel :: JBang :: MCP 34.3s SUCCESS
Camel :: JBang :: Plugin :: Validate 27.7s SUCCESS
Camel :: Catalog :: Camel Catalog 20.9s SUCCESS
Camel :: YAML DSL :: Validator 19.9s SUCCESS
Camel :: Component DSL 18.9s SUCCESS
Camel :: YAML DSL 18.4s SUCCESS
Camel :: Docs 14.0s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 12.1s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 11.2s SUCCESS
Camel :: Kamelet Main 10.8s SUCCESS
Camel :: JBang :: Plugin :: Testing 8.9s SUCCESS
Camel :: YAML DSL :: Deserializers 7.2s SUCCESS
Camel :: Catalog :: Camel Route Parser 7.1s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 6.3s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 5.8s SUCCESS
Camel :: All Components Sync point 4.1s SUCCESS
Camel :: Catalog :: Maven 2.5s SUCCESS
Camel :: YAML DSL :: Maven Plugins 2.4s SUCCESS
Camel :: JBang :: Integration tests 1.5s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.4s SUCCESS
Camel :: Catalog :: Dummy Component 1.3s SUCCESS
Camel :: Assembly 1.3s SUCCESS
Camel :: Coverage 1.1s SUCCESS
Camel :: Endpoint DSL :: Support 1.1s SUCCESS
Camel :: Catalog :: Console 1.0s SUCCESS
Camel :: JBang :: Plugin :: Generate 0.9s SUCCESS
Camel :: JBang :: Main 0.8s SUCCESS
Camel :: Launcher :: Container 0.7s SUCCESS
Camel :: JBang :: Plugin :: MCP 0.6s SUCCESS
Camel :: Endpoint DSL n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a
Camel :: JBang :: Plugin :: TUI n/a
Camel :: Milo n/a

Top 20 slowest modules:

  • Camel :: Launcher (41.6s)
  • Camel :: JBang :: MCP (34.3s)
  • Camel :: JBang :: Plugin :: Validate (27.7s)
  • Camel :: Catalog :: Camel Catalog (20.9s)
  • Camel :: YAML DSL :: Validator (19.9s)
  • Camel :: Component DSL (18.9s)
  • Camel :: YAML DSL (18.4s)
  • Camel :: Docs (14.0s)
  • Camel :: Catalog :: Camel Report Maven Plugin (12.1s)
  • Camel :: JBang :: Plugin :: Kubernetes (11.2s)
  • Camel :: Kamelet Main (10.8s)
  • Camel :: JBang :: Plugin :: Testing (8.9s)
  • Camel :: YAML DSL :: Deserializers (7.2s)
  • Camel :: Catalog :: Camel Route Parser (7.1s)
  • Camel :: YAML DSL :: Validator Maven Plugin (6.3s)
  • Camel :: Catalog :: Suggest (deprecated) (5.8s)
  • Camel :: All Components Sync point (4.1s)
  • Camel :: Catalog :: Maven (2.5s)
  • Camel :: YAML DSL :: Maven Plugins (2.4s)
  • Camel :: JBang :: Integration tests (1.5s)

⚙️ View full build and test results

@davsclaus davsclaus added this to the 4.24.0 milestone Oct 19, 2026
@davsclaus davsclaus added the bug Something isn't working label Oct 19, 2026
@davsclaus
davsclaus merged commit 7525d7c into apache:main Oct 19, 2026
7 checks passed
@etzel-mes
etzel-mes deleted the fix/CAMEL-25690-milo-producer-unknown-namespace branch October 23, 2026 06:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants