Skip to content

CAMEL-25626: camel-milo - deliver a data change with a Bad status even when its value is null - #27908

Merged
davsclaus merged 3 commits into
apache:mainfrom
etzel-mes:fix/CAMEL-25626-milo-bad-values
Oct 16, 2026
Merged

davsclaus merged 3 commits into
apache:mainfrom
etzel-mes:fix/CAMEL-25626-milo-bad-values

Conversation

@etzel-mes

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

Copy link
Copy Markdown
Contributor

Description

CAMEL-25626.

With the default omitNullValues=true, the milo-client consumer dropped every data change whose value was null, regardless of its status code. OPC UA Part 4 requires a server to send a null value together with a Bad status, so dropping null values drops every Bad status: a sensor failure, a removed node or a monitored item that could not be created (#27780, #27906) never reached the route unless omitNullValues=false was set.

Why this matters in automation: a route that feeds a historian, an MES, a UNS topic or an alarm evaluates the last value it received. When the quality of a tag goes Bad and the route never hears about it, the stale value keeps being treated as current, and the plant acts on a number that nobody is measuring any more. Quality propagation is the first thing an OT integration is expected to get right; the component should deliver it by default.

The change:

  • omitNullValues now omits only a data change whose value is null and whose status is Good. A data change with a Bad or Uncertain status is always delivered; the body is the DataValue as before, the route reads the status code from it and CamelMiloNodeId names the node.
  • This includes the first notification of a monitored item, which carries the current value of the node at startup and after a reconnect and can be a Bad status: a milo-server item that was not written yet, or Bad_WaitingForInitialData from a server that has no value yet. CAMEL-18144 added omitNullValues to skip that notification and to keep the unit tests stable. It is now delivered because it carries the quality of the node; the tests write the value before they start their consumers instead.
  • The default still omits a Good null that follows a Bad status, so a route does not see a node recover to a null value; the component doc says so and when to set omitNullValues=false.
  • No new option. The condition lives in one small predicate in MiloClientConsumer.
  • Tests: MonitorNodesTest#notGoodStatusWithNullValueIsDeliveredByDefault (default option): a Good null is omitted, an Uncertain null and a Bad status are delivered, a later Good write still arrives; it fails without the fix (2 expected, 0 received). MonitorItemLifecycleTest#itemThatWasNotWrittenYetReportsABadStatusAtStartup pins the Bad status at startup. MonitorItemLifecycleTest, MonitorItemTest and MonitorItemMultiConnectionsTest start their consumers after the first write, and omitNullValues=false was removed from the existing tests.
  • Endpoint javadoc, generated JSON, catalog copies, endpoint DSL, component docs and a 4.24 upgrade-guide note.

Note on generated files: the endpoint DSL javadoc was edited by hand, wrapped as the generator does, because its generator needs the full reactor; the catalog files are copies of the module files. If the regen check differs, I will apply the patch it posts.

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

🤖 Generated with Claude Code

…n when its value is null

With the default omitNullValues=true, the milo-client consumer dropped every
data change whose value was null, regardless of its status code. An OPC UA
server reports a Bad status with a null value, for example a node that does
not exist or a monitored item that could not be created, so the route never
learned that a node went bad unless omitNullValues=false was set.

A data change whose status is Bad or Uncertain is now delivered even when
its value is null. The exchange body is the DataValue as before, so the
route reads the status code from it and the CamelMiloNodeId header names
the node.

omitNullValues still omits a data change whose value is null and whose
status is Good, for example the initial value at startup.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

@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.

Thanks for this fix — the logic is correct and the PR is well-structured. One minor observation below about nullability contracts in the new helper method.

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

…mitNullValues check

DataValue.getValue() and getStatusCode() are non-null in Milo 1.x; only Variant.getValue() is nullable, so the extra null checks were dead code.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

@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.

Previous finding addressed. The dead null checks on getValue() and getStatusCode() have been removed (891bf76) — the author verified against Milo 1.1.8 that these are @NullMarked non-null. Removing them is the right call.

One minor note below on the remaining value == null guard, but it doesn't block merge — the logic is correct and the behavioral fix is well-tested and documented.

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

@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.

Claude Code on behalf of davsclaus

Thanks Serdar, this is a good change to make. Quality propagation matters for OT routes, the predicate is small, and the new test covers the Good, Uncertain and Bad cases. I found one issue that I think needs fixing before merge. The PR's premise is that the startup value is a null with a Good status, but that is not always true. I traced the code below; I could not run it because CI has not run on this PR yet.

The startup value can be Bad, and it is now delivered

  • CamelServerItem starts every item as new DataValue(StatusCode.BAD) (components/camel-milo/src/main/java/org/apache/camel/component/milo/server/internal/CamelServerItem.java, line 54). Milo samples a monitored item through a read that carries the session (SubscriptionModel builds new ReadContext(server, session)), so the attribute filter returns getDataValue(). A producer write only updates that field through update(), which is how the client sees Foo. It follows that a milo-client consumer that subscribes to a milo-server item before the first write receives a Bad status with a null value as its first notification. Before this PR, the default omitNullValues=true dropped that notification; with this PR it is delivered. Real servers can do the same in the initial notification, for example with Bad_WaitingForInitialData.

  • Some existing tests start their consumer before the first write and expect the first message to be the first written value. They appear to rely on that drop:

    • MonitorItemLifecycleTest#restartedConsumerReceivesEachValueOnce and #unknownNodeReceivesBadStatus: route item1 auto-starts, then the test expects expectedMessageCount(1) with message(0) = Foo.
    • MonitorItemTest#testMonitorItem1: message(0) = Foo on all four consumers.
    • MonitorItemMultiConnectionsTest#testMonitorItem1: test1 expects exactly 1 message.

    I would expect these to fail once CI runs. If they passed in your local -Psourcecheck verify run, please tell me what I am missing in the trace.

  • Delivering that startup Bad status is arguably right, given the rationale of the PR. Either way, please update three things:

    1. The text. The endpoint javadoc (and with it the generated JSON, catalog and endpoint DSL), the component doc and the 4.24 upgrade-guide note all say the startup value is a null with a Good status and is still omitted. For the Camel server, and for servers that report Bad_WaitingForInitialData, it is not. The upgrade note should tell users that routes may now receive a Bad status at startup and can filter on the status code of the DataValue if they do not want it.
    2. The tests above. Write the value before the route starts (autoStartup(false), as MonitorNodesTest already does), or assert the startup Bad status explicitly.
    3. A test for this case, so the startup behaviour is pinned: a consumer that subscribes to a milo-server item before anything was written.

The generated files: you noted they were edited by hand. The CI regen check will show whether they match the generator output.

This review does not replace specialized AI review tools or static analysis such as SonarCloud.

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

Comment thread docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc Outdated
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

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

🔬 Scalpel shadow comparison — Scalpel: 8 of 705 tested, 24 compile-only — current: 8 all tested

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

Skip-tests mode would test 8 modules (4 direct + 7 downstream), skip tests for 24 (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/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json, components/camel-milo/src/main/docs/milo-client-component.adoc, components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConsumer.java, components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientEndpoint.java, components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemLifecycleTest.java, components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemMultiConnectionsTest.java, components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemTest.java, components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorNodesTest.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 (24)
  • apache-camel
  • camel-allcomponents
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • 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, 5m 22s total)

Total reactor time: 5m 22s

Module Duration Status
Camel :: JBang :: Plugin :: TUI 56.0s SUCCESS
Camel :: Milo 50.3s SUCCESS
Camel :: Launcher 27.4s SUCCESS
Camel :: JBang :: Plugin :: Validate 26.5s SUCCESS
Camel :: JBang :: MCP 25.7s SUCCESS
Camel :: Catalog :: Camel Catalog 15.9s SUCCESS
Camel :: YAML DSL :: Validator 15.3s SUCCESS
Camel :: Component DSL 15.1s SUCCESS
Camel :: YAML DSL 15.1s SUCCESS
Camel :: Docs 9.2s SUCCESS
Camel :: Kamelet Main 9.0s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 8.6s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 7.1s SUCCESS
Camel :: JBang :: Plugin :: Testing 5.8s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 5.2s SUCCESS
Camel :: Catalog :: Camel Route Parser 5.2s SUCCESS
Camel :: YAML DSL :: Deserializers 4.9s SUCCESS
Camel :: All Components Sync point 3.7s SUCCESS
Camel :: Catalog :: Maven 2.1s SUCCESS
Camel :: YAML DSL :: Maven Plugins 2.0s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 1.9s SUCCESS
Camel :: Assembly 1.8s SUCCESS
Camel :: Coverage 1.2s SUCCESS
Camel :: JBang :: Integration tests 1.1s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.1s SUCCESS
Camel :: Catalog :: Dummy Component 1.0s SUCCESS
Camel :: JBang :: Plugin :: Generate 0.9s SUCCESS
Camel :: Catalog :: Console 0.6s SUCCESS
Camel :: Endpoint DSL :: Support 0.6s SUCCESS
Camel :: Launcher :: Container 0.6s SUCCESS
Camel :: JBang :: Main 0.5s SUCCESS
Camel :: JBang :: Plugin :: MCP 0.4s SUCCESS
Camel :: Endpoint DSL n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a

Top 20 slowest modules:

  • Camel :: JBang :: Plugin :: TUI (56.0s)
  • Camel :: Milo (50.3s)
  • Camel :: Launcher (27.4s)
  • Camel :: JBang :: Plugin :: Validate (26.5s)
  • Camel :: JBang :: MCP (25.7s)
  • Camel :: Catalog :: Camel Catalog (15.9s)
  • Camel :: YAML DSL :: Validator (15.3s)
  • Camel :: Component DSL (15.1s)
  • Camel :: YAML DSL (15.1s)
  • Camel :: Docs (9.2s)
  • Camel :: Kamelet Main (9.0s)
  • Camel :: JBang :: Plugin :: Kubernetes (8.6s)
  • Camel :: YAML DSL :: Validator Maven Plugin (7.1s)
  • Camel :: JBang :: Plugin :: Testing (5.8s)
  • Camel :: Catalog :: Camel Report Maven Plugin (5.2s)
  • Camel :: Catalog :: Camel Route Parser (5.2s)
  • Camel :: YAML DSL :: Deserializers (4.9s)
  • Camel :: All Components Sync point (3.7s)
  • Camel :: Catalog :: Maven (2.1s)
  • Camel :: YAML DSL :: Maven Plugins (2.0s)

⚙️ View full build and test results

The first notification of a monitored item carries the current value of
the node. That can be a Bad status with a null value: a milo-server item
that was not written yet, or Bad_WaitingForInitialData from a server
that has no value yet. The consumer delivers it like any other Bad
status, so the option and consumer javadoc no longer name the startup
value as an omitted value, and the component doc and the 4.24 upgrade
note say that a route may receive such a status and how to skip it.

The tests that expected the first written value as their first message
now write it before they start their consumers. In MonitorItemTest the
client that samples once per second now always receives that value, so
its bounds are one higher. A new test pins the Bad status of an item
that was not written yet.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@etzel-mes

Copy link
Copy Markdown
Contributor Author

Thanks, confirmed: a milo-server item starts as new DataValue(StatusCode.BAD), the first notification of a monitored item carries that value, and with this PR the consumer delivers it. That is intended, since it is quality information like Bad_WaitingForInitialData, so the texts and the tests were wrong, not the predicate. CAMEL-18144 added omitNullValues to skip exactly this notification; the PR description now says so. Fixed in 23f226f:

  1. Texts. The option javadoc (and with it the generated JSON, catalog and endpoint DSL) and the MiloClientConsumer javadoc no longer give the startup value as an example. The 4.24 upgrade note now has its own section: a route may receive a Bad status with a null value at startup and after a reconnect, for example Bad_WaitingForInitialData or the status of a milo-server item that was not written yet; it checks getStatusCode() of the DataValue body before it uses the value, and filters on the status code to skip it. The component doc says the same and adds that the default still omits a Good null after a Bad status.
  2. Tests. MonitorItemLifecycleTest, MonitorItemTest and MonitorItemMultiConnectionsTest now write the value before they start their consumers (autoStartup(false), as in MonitorNodesTest). In MonitorItemTest, the client that samples once per second now always gets Foo as its first message; it got 6 messages, the old maximum, in every local run, so its bounds are one higher (5 to 7).
  3. New test. MonitorItemLifecycleTest#itemThatWasNotWrittenYetReportsABadStatusAtStartup starts a consumer on an item that was not written yet and expects a Bad DataValue with a null value, then the written value.

Why they passed: the trace is right, but the tests did not fail reliably. CI on 891bf76, which finished after your review, was green on JDK 17 and 25 with no recovered flaky tests. MonitorItemLifecycleTest and MonitorItemMultiConnectionsTest wrote Foo right after the context started and passed whenever the write landed before the server's first sample, or the count was checked before the second notification arrived. With -Dsurefire.rerunFailingTestsCount=0, MonitorItemLifecycleTest#restartedConsumerReceivesEachValueOnce failed in one run of nine on my machine (message(0) expected Foo but was null); the full profile reruns a failed test up to twice. In MonitorItemTest the message(i) checks are added after the messages arrived, so in the mock's default fail-fast mode they never run; only its counters could fail. With the changes, the four classes passed eight runs in a row with reruns disabled, and the module -Psourcecheck verify passed (57 tests, 1 skipped).

Real servers with this branch: node-opcua 2.187.9 sends Bad_WaitingForInitialData (or Uncertain_InitialValue) with a null value as the first notification, and Microsoft OPC PLC 2.15.4 sends Bad_NoCommunication for a node in a bad state; the route now receives them by default. The default still drops Good notifications with a null value, which OPC PLC sends for null nodes, so the option keeps a purpose.

The endpoint DSL javadoc is still edited by hand (its generator needs the full reactor), wrapped like the generator output; the regen check passed on 891bf76, and if it differs this time, I will apply its patch.

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.

All findings from the previous review round have been addressed in the latest commits. The fix is correct: isNullValueWithGoodStatus now properly gates null-value delivery on status quality, allowing DataValues with non-Good status codes (e.g. Uncertain) to pass through even when their inner value is null — which is the intended OPC-UA semantics. The NPE concern is a non-issue per Milo's @NotNull contract on DataValue.getValue(). The value == null arm at the top of the guard is acceptable defensive coding. Updated test bounds (+1 min/max) correctly account for the autoStartup(false) + startRoute() sequence that ensures the consumer sees "Foo" as the first notification.

No milestone set — consider targeting the next Camel release milestone.

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

@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.

Claude Code on behalf of davsclaus

Thanks @etzel-mes, all points from my previous review are addressed:

  • The option Javadoc (and with it the JSON, catalog and endpoint DSL), the consumer Javadoc and the docs no longer describe the startup value as a Good null.
  • The upgrade guide now tells users that a Bad status may arrive at startup and after a reconnect, and how to filter it.
  • The tests write their value before the consumers start, and a new test pins the startup Bad status.

Heads-up for merge order: this PR and #27910 conflict in MonitorItemLifecycleTest. With item1 now autoStartup(false), the recreate tests in #27910 will need to start item1 after the first write, whichever of the two merges second.

CI has not run on this head yet (the workflows are waiting for approval), so the regen check on the hand-edited endpoint DSL still has to confirm it; this approval assumes it goes green.

This review does not replace specialized AI review tools or static analysis such as SonarCloud.

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants