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 5961fc46e56b CAMEL-25626: camel-milo - deliver a data change with a
Bad status even when its value is null
5961fc46e56b is described below
commit 5961fc46e56b43cf29a022fb7e93a46041315753
Author: Serdar Gökay <[email protected]>
AuthorDate: Fri Oct 16 17:59:44 2026 +0300
CAMEL-25626: camel-milo - deliver a data change with a Bad status even when
its value is null
[CAMEL-25626](https://issues.apache.org/jira/browse/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.
Closes #27908
Co-authored-by: Claude Fable 5.1 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../camel/catalog/components/milo-client.json | 2 +-
.../camel/catalog/docs/milo-client-component.adoc | 16 +++++++--
.../camel/component/milo/client/milo-client.json | 2 +-
.../src/main/docs/milo-client-component.adoc | 16 +++++++--
.../component/milo/client/MiloClientConsumer.java | 13 ++++++--
.../component/milo/client/MiloClientEndpoint.java | 3 +-
.../component/milo/MonitorItemLifecycleTest.java | 39 +++++++++++++++++-----
.../milo/MonitorItemMultiConnectionsTest.java | 10 +++---
.../camel/component/milo/MonitorItemTest.java | 38 +++++++++++----------
.../camel/component/milo/MonitorNodesTest.java | 29 +++++++++++++++-
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 13 +++++++-
.../dsl/MiloClientEndpointBuilderFactory.java | 18 ++++++----
12 files changed, 152 insertions(+), 47 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json
index 8b3ac0766cbb..8b32e5c1d6ff 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json
@@ -70,7 +70,7 @@
"method": { "index": 8, "kind": "parameter", "displayName": "Method",
"group": "common", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "The method definition (see Method ID)" },
"monitorFilterType": { "index": 9, "kind": "parameter", "displayName":
"Monitor Filter Type", "group": "common", "label": "", "required": false,
"type": "enum", "javaType":
"org.apache.camel.component.milo.client.MonitorFilterType", "enum": [
"dataChangeFilter" ], "deprecated": false, "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.milo.client.MonitorFilterConfiguration",
"configurationField": "monitorFilterConfiguration", "description": "Monito [...]
"node": { "index": 10, "kind": "parameter", "displayName": "Node",
"group": "common", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "The node definition (see Node ID)" },
- "omitNullValues": { "index": 11, "kind": "parameter", "displayName": "Omit
Null Values", "group": "common", "label": "", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": true, "description": "Omit notifications in
case of null values" },
+ "omitNullValues": { "index": 11, "kind": "parameter", "displayName": "Omit
Null Values", "group": "common", "label": "", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": true, "description": "Omit a data change whose
value is null and whose status is Good. A data change with a Bad or Uncertain
status is always delivered." },
"queueSize": { "index": 12, "kind": "parameter", "displayName": "Queue
Size", "group": "common", "label": "", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "description": "The queue size used for OPC UA subscriptions.
If not set, a queue size of 1 is requested." },
"samplingInterval": { "index": 13, "kind": "parameter", "displayName":
"Sampling Interval", "group": "common", "label": "", "required": false, "type":
"number", "javaType": "java.lang.Double", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "1000.0", "description": "The sampling
interval in milliseconds" },
"nodes": { "index": 14, "kind": "parameter", "displayName": "Nodes",
"group": "consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "Comma-separated node definitions to monitor
with one consumer, instead of node (see Node ID)" },
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 e04f72f92737..046501c41d7d 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,8 +149,20 @@ 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.
-To see such nodes, set `omitNullValues=false`: the consumer then delivers one
exchange with the bad status code for that
-node, and the `CamelMiloNodeId` header names it.
+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
+
+A data change with a Bad or Uncertain status is always delivered, even when
its value is null; `omitNullValues` only
+omits a null value with a Good status. This includes the first notification of
a monitored item: it carries the current
+value of the node, at startup and again after a reconnect, so it can be a Bad
status such as `Bad_WaitingForInitialData`
+or the status of a `milo-server` item that was not written yet. To skip such a
notification, a route filters on its
+status code (`getStatusCode()` of the `DataValue` body); a filter on
`isGood()` would also drop the Bad status of a
+failed sensor.
+
+The default `omitNullValues=true` also omits a null value with a Good status
that follows a Bad status, so the route
+does not learn that the node recovered. Set `omitNullValues=false` for a node
whose value can be null, such as a
+`String`.
=== Method ID
diff --git
a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json
b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json
index 8b3ac0766cbb..8b32e5c1d6ff 100644
---
a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json
+++
b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json
@@ -70,7 +70,7 @@
"method": { "index": 8, "kind": "parameter", "displayName": "Method",
"group": "common", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "The method definition (see Method ID)" },
"monitorFilterType": { "index": 9, "kind": "parameter", "displayName":
"Monitor Filter Type", "group": "common", "label": "", "required": false,
"type": "enum", "javaType":
"org.apache.camel.component.milo.client.MonitorFilterType", "enum": [
"dataChangeFilter" ], "deprecated": false, "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.milo.client.MonitorFilterConfiguration",
"configurationField": "monitorFilterConfiguration", "description": "Monito [...]
"node": { "index": 10, "kind": "parameter", "displayName": "Node",
"group": "common", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "The node definition (see Node ID)" },
- "omitNullValues": { "index": 11, "kind": "parameter", "displayName": "Omit
Null Values", "group": "common", "label": "", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": true, "description": "Omit notifications in
case of null values" },
+ "omitNullValues": { "index": 11, "kind": "parameter", "displayName": "Omit
Null Values", "group": "common", "label": "", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": true, "description": "Omit a data change whose
value is null and whose status is Good. A data change with a Bad or Uncertain
status is always delivered." },
"queueSize": { "index": 12, "kind": "parameter", "displayName": "Queue
Size", "group": "common", "label": "", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "description": "The queue size used for OPC UA subscriptions.
If not set, a queue size of 1 is requested." },
"samplingInterval": { "index": 13, "kind": "parameter", "displayName":
"Sampling Interval", "group": "common", "label": "", "required": false, "type":
"number", "javaType": "java.lang.Double", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "1000.0", "description": "The sampling
interval in milliseconds" },
"nodes": { "index": 14, "kind": "parameter", "displayName": "Nodes",
"group": "consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "Comma-separated node definitions to monitor
with one consumer, instead of node (see Node ID)" },
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 e04f72f92737..046501c41d7d 100644
--- a/components/camel-milo/src/main/docs/milo-client-component.adoc
+++ b/components/camel-milo/src/main/docs/milo-client-component.adoc
@@ -149,8 +149,20 @@ 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.
-To see such nodes, set `omitNullValues=false`: the consumer then delivers one
exchange with the bad status code for that
-node, and the `CamelMiloNodeId` header names it.
+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
+
+A data change with a Bad or Uncertain status is always delivered, even when
its value is null; `omitNullValues` only
+omits a null value with a Good status. This includes the first notification of
a monitored item: it carries the current
+value of the node, at startup and again after a reconnect, so it can be a Bad
status such as `Bad_WaitingForInitialData`
+or the status of a `milo-server` item that was not written yet. To skip such a
notification, a route filters on its
+status code (`getStatusCode()` of the `DataValue` body); a filter on
`isGood()` would also drop the Bad status of a
+failed sensor.
+
+The default `omitNullValues=true` also omits a null value with a Good status
that follows a Bad status, so the route
+does not learn that the node recovered. Set `omitNullValues=false` for a node
whose value can be null, such as a
+`String`.
=== Method ID
diff --git
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConsumer.java
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConsumer.java
index a6b6e0c2cd1c..59e0c90aabc1 100644
---
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConsumer.java
+++
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConsumer.java
@@ -116,8 +116,8 @@ public class MiloClientConsumer extends DefaultConsumer {
private void handleValueUpdate(final String node, final DataValue value) {
LOG.debug("Handle item update - {} = {}", node, value);
- if (omitNullValues && (value == null || value.getValue() == null ||
value.getValue().getValue() == null)) {
- LOG.debug("Handle item update omitted due to null values (see
omitNullValues parameter)");
+ if (omitNullValues && isNullValueWithGoodStatus(value)) {
+ LOG.debug("Handle item update omitted due to a null value with a
Good status (see omitNullValues parameter)");
return;
}
@@ -131,6 +131,15 @@ public class MiloClientConsumer extends DefaultConsumer {
}
}
+ /**
+ * The data change that omitNullValues omits: a null value with a Good
status. A data change with a Bad or Uncertain
+ * status is delivered even when its value is null.
+ */
+ private static boolean isNullValueWithGoodStatus(final DataValue value) {
+ return value == null
+ || (value.getValue().getValue() == null &&
value.getStatusCode().isGood());
+ }
+
private void mapToMessage(final DataValue value, final Message message) {
if (value != null) {
Messages.fillFromDataValue(value, message);
diff --git
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientEndpoint.java
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientEndpoint.java
index 752e4adb114d..afc7cc34ec2f 100644
---
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientEndpoint.java
+++
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientEndpoint.java
@@ -91,7 +91,8 @@ public class MiloClientEndpoint extends DefaultEndpoint {
private boolean defaultAwaitWrites;
/**
- * Omit notifications in case of null values
+ * Omit a data change whose value is null and whose status is Good. A data
change with a Bad or Uncertain status is
+ * always delivered.
*/
@UriParam(defaultValue = "true")
private boolean omitNullValues = true;
diff --git
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemLifecycleTest.java
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemLifecycleTest.java
index b5f50b241691..cec8822d6eb0 100644
---
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemLifecycleTest.java
+++
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemLifecycleTest.java
@@ -40,6 +40,7 @@ import org.junit.jupiter.api.Timeout;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -77,15 +78,13 @@ public class MonitorItemLifecycleTest extends
AbstractMiloServerTest {
= MILO_CLIENT +
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, "myitem2");
private static final String MILO_CLIENT_UNKNOWN_ITEM
- = MILO_CLIENT +
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, "unknownItem")
- + "&omitNullValues=false";
+ = MILO_CLIENT +
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, "unknownItem");
private static final String MILO_CLIENT_UNKNOWN_ITEM_WRITE_BACK
- = MILO_CLIENT +
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI,
"unknownItemWriteBack")
- + "&omitNullValues=false";
+ = MILO_CLIENT +
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI,
"unknownItemWriteBack");
private static final String MILO_CLIENT_ITEMS_1_2
- = MILO_CLIENT_BASE + "&omitNullValues=false&nodes=RAW(" +
NODE_ITEM_1 + "," + NODE_ITEM_2 + ")";
+ = MILO_CLIENT_BASE + "&nodes=RAW(" + NODE_ITEM_1 + "," +
NODE_ITEM_2 + ")";
private static final String MOCK_ITEM_1 = "mock:item1";
@@ -126,10 +125,10 @@ public class MonitorItemLifecycleTest extends
AbstractMiloServerTest {
from(DIRECT_START_1).to(MILO_SERVER_ITEM_1);
from(DIRECT_START_2).to(MILO_SERVER_ITEM_2);
-
from(resolve(MILO_CLIENT_ITEM_1)).routeId("item1").to(MOCK_ITEM_1);
from(resolve(MILO_CLIENT_UNKNOWN_ITEM)).routeId("unknownItem").to(MOCK_UNKNOWN_ITEM);
// the consumers deliver the current values while they start,
so the tests start these routes
+
from(resolve(MILO_CLIENT_ITEM_1)).routeId("item1").autoStartup(false).to(MOCK_ITEM_1);
from(resolve(MILO_CLIENT_ITEM_2)).routeId("item2").autoStartup(false).to(MOCK_ITEM_2);
from(resolve(MILO_CLIENT_ITEMS_1_2)).routeId("items12").autoStartup(false).to(MOCK_ITEMS_1_2);
@@ -145,9 +144,11 @@ public class MonitorItemLifecycleTest extends
AbstractMiloServerTest {
@Test
void restartedConsumerReceivesEachValueOnce() throws Exception {
+ producer1.sendBody("Foo");
+
item1Endpoint.expectedMessageCount(1);
testBody(item1Endpoint.message(0), assertGoodValue("Foo"));
- producer1.sendBody("Foo");
+ context.getRouteController().startRoute("item1");
item1Endpoint.assertIsSatisfied();
// the other consumers keep the shared connection open
@@ -180,12 +181,34 @@ public class MonitorItemLifecycleTest extends
AbstractMiloServerTest {
unknownItemEndpoint.assertIsSatisfied();
// the other consumers of the connection are not affected
+ producer1.sendBody("Foo");
+
item1Endpoint.expectedMessageCount(1);
testBody(item1Endpoint.message(0), assertGoodValue("Foo"));
- producer1.sendBody("Foo");
+ context.getRouteController().startRoute("item1");
item1Endpoint.assertIsSatisfied();
}
+ @Test
+ void itemThatWasNotWrittenYetReportsABadStatusAtStartup() throws Exception
{
+ // the first notification carries the current value of the item, which
is a Bad status without a value until
+ // the item is written; the consumer delivers it like any other Bad
status
+ item2Endpoint.expectedMessageCount(1);
+ item2Endpoint.expectedHeaderReceived(MiloConstants.HEADER_NODE_ID,
NODE_ITEM_2);
+ testBody(item2Endpoint.message(0), value -> {
+ assertTrue(value.getStatusCode().isBad(),
value.getStatusCode().toString());
+ assertNull(value.getValue().getValue());
+ });
+ context.getRouteController().startRoute("item2");
+ item2Endpoint.assertIsSatisfied();
+
+ item2Endpoint.reset();
+ item2Endpoint.expectedMessageCount(1);
+ testBody(item2Endpoint.message(0), assertGoodValue("Bar"));
+ producer2.sendBody("Bar");
+ item2Endpoint.assertIsSatisfied();
+ }
+
@Test
@Timeout(value = 60, unit = TimeUnit.SECONDS)
void badStatusIsDeliveredWithoutHoldingTheConnectionLock() throws
Exception {
diff --git
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemMultiConnectionsTest.java
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemMultiConnectionsTest.java
index a673c6d3bf0e..152582e3bb00 100644
---
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemMultiConnectionsTest.java
+++
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemMultiConnectionsTest.java
@@ -86,9 +86,10 @@ public class MonitorItemMultiConnectionsTest extends
AbstractMiloServerTest {
public void configure() {
from(DIRECT_START_1).to(MILO_SERVER_ITEM_1);
- from(resolve(MILO_CLIENT_ITEM_C1_1)).to(MOCK_TEST_1);
- from(resolve(MILO_CLIENT_ITEM_C2_1)).to(MOCK_TEST_2);
- from(resolve(MILO_CLIENT_ITEM_C3_1)).to(MOCK_TEST_3);
+ // the consumers deliver the current value while they start,
so the test starts these routes
+
from(resolve(MILO_CLIENT_ITEM_C1_1)).autoStartup(false).to(MOCK_TEST_1);
+
from(resolve(MILO_CLIENT_ITEM_C2_1)).autoStartup(false).to(MOCK_TEST_2);
+
from(resolve(MILO_CLIENT_ITEM_C3_1)).autoStartup(false).to(MOCK_TEST_3);
}
};
}
@@ -110,8 +111,9 @@ public class MonitorItemMultiConnectionsTest extends
AbstractMiloServerTest {
this.test3Endpoint.setExpectedCount(0);
this.test3Endpoint.setSleepForEmptyTest(5_000);
- // set server value
+ // set server value, then start the consumers
this.producer1.sendBody("Foo");
+ context.getRouteController().startAllRoutes();
// assert
MockEndpoint.assertIsSatisfied(context);
diff --git
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemTest.java
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemTest.java
index 07509dea2348..3181f2457a35 100644
---
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemTest.java
+++
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemTest.java
@@ -108,10 +108,11 @@ public class MonitorItemTest extends
AbstractMiloServerTest {
public void configure() {
from(DIRECT_START_1).to(MILO_SERVER_ITEM_1);
- from(resolve(MILO_CLIENT_ITEM_C1_1)).to(MOCK_TEST_1);
- from(resolve(MILO_CLIENT_ITEM_C1_2)).to(MOCK_TEST_2);
- from(resolve(MILO_CLIENT_ITEM_C1_3)).to(MOCK_TEST_3);
- from(resolve(MILO_CLIENT_ITEM_C2_1)).to(MOCK_TEST_4);
+ // the consumers deliver the current value while they start,
so the test starts these routes
+
from(resolve(MILO_CLIENT_ITEM_C1_1)).autoStartup(false).to(MOCK_TEST_1);
+
from(resolve(MILO_CLIENT_ITEM_C1_2)).autoStartup(false).to(MOCK_TEST_2);
+
from(resolve(MILO_CLIENT_ITEM_C1_3)).autoStartup(false).to(MOCK_TEST_3);
+
from(resolve(MILO_CLIENT_ITEM_C2_1)).autoStartup(false).to(MOCK_TEST_4);
}
};
}
@@ -162,13 +163,13 @@ public class MonitorItemTest extends
AbstractMiloServerTest {
/*
* test3Endpoint is related to client C1_3 with:
requestedPublishingInterval=2000, samplingInterval=1000 from
- * default, queueSize=4. It should get 1 (or 2 depending on exact
timing) of the first 3 messages because of
- * samplingInterval and an update pause of 2 seconds after they are
sent. From the rest 16 messages, sent in
- * about 3 seconds, only 3 or 4 should be sampled. So minimum is 4,
maximum is
- * 6 messages
+ * default, queueSize=4. It should get Foo, the value when it starts,
and 1 (or 2 depending on exact timing) of
+ * the next 3 messages because of samplingInterval and an update pause
of 2 seconds after they are sent. From
+ * the rest 16 messages, sent in about 3 seconds, only 3 or 4 should
be sampled. So minimum is 5, maximum is
+ * 7 messages
*/
test3Endpoint.reset();
- test3Endpoint.setMinimumExpectedMessageCount(4); // One or 2 of the
first 3, plus 3 or 4 of the remaining 16
+ test3Endpoint.setMinimumExpectedMessageCount(5); // Foo, one or 2
of the next 3, plus 3 or 4 of the remaining 16
test3Endpoint.setAssertPeriod(3000);
/*
@@ -183,19 +184,20 @@ public class MonitorItemTest extends
AbstractMiloServerTest {
test4Endpoint.setMinimumExpectedMessageCount(12); // the first 3,
plus at least 3*3 more from rest (if they fall to 3 periods)
test4Endpoint.setAssertPeriod(3000);
- // Allow time for OPC UA client-server connection to establish
- await().pollDelay(1, TimeUnit.SECONDS).untilAsserted(() -> {
- // Connection should be established
- });
-
// Debug: Check if server is configured correctly
LOG.info("Server Port: {}", this.getServerPort());
LOG.info("Client URI resolved: {}", resolve(MILO_CLIENT_ITEM_C1_1));
- // set server values sent faster than requestedPublishingInterval so
they must be put to queue
+ // the first value is written before the consumers start, so it is the
first notification of each client
this.producer1.sendBody("Foo");
- await().pollDelay(time, TimeUnit.MILLISECONDS).untilAsserted(() -> {
+ context.getRouteController().startAllRoutes();
+
+ // Allow time for OPC UA client-server connection to establish
+ await().pollDelay(1, TimeUnit.SECONDS).untilAsserted(() -> {
+ // Connection should be established
});
+
+ // set server values sent faster than requestedPublishingInterval so
they must be put to queue
this.producer1.sendBody("Bar");
await().pollDelay(time, TimeUnit.MILLISECONDS).untilAsserted(() -> {
});
@@ -241,9 +243,9 @@ public class MonitorItemTest extends AbstractMiloServerTest
{
Exchange last = receivedExchanges.get(receivedExchanges.size() - 1);
assertGoodValue("Done").accept((DataValue) last.getIn().getBody());
- // The third client should get no more than 6 messages
+ // The third client should get no more than 7 messages
int count3 = this.test3Endpoint.getReceivedCounter();
- assertTrue(count3 <= 6, "Not enough messages have been dropped, but
should because of queueSize");
+ assertTrue(count3 <= 7, "Not enough messages have been dropped, but
should because of queueSize");
// The fourth client should get no more than 14 messages, because it
is very sensitive on the timing we allow 1 more
int count4 = this.test4Endpoint.getReceivedCounter();
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 18460c74164a..cef04fa5f63c 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
@@ -34,9 +34,11 @@ 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.StatusCode;
+import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
@@ -61,7 +63,7 @@ public class MonitorNodesTest extends AbstractMiloServerTest {
private static final String MILO_CLIENT_NODES = MILO_CLIENT +
"&nodes=RAW(" + NODE_1 + "," + NODE_2 + ")";
private static final String MILO_CLIENT_UNKNOWN_NODE
- = MILO_CLIENT + "&omitNullValues=false&nodes=RAW(" + NODE_1 + ","
+ UNKNOWN_NODE + "," + NODE_2 + ")";
+ = MILO_CLIENT + "&nodes=RAW(" + NODE_1 + "," + UNKNOWN_NODE + ","
+ NODE_2 + ")";
private static final String MILO_CLIENT_DUPLICATES
= MILO_CLIENT + "&nodes=RAW( " + NODE_1 + " ,, " + NODE_2 + " ," +
NODE_1 + ")";
@@ -144,6 +146,31 @@ public class MonitorNodesTest extends
AbstractMiloServerTest {
unknownNodeEndpoint.assertIsSatisfied();
}
+ @Test
+ void notGoodStatusWithNullValueIsDeliveredByDefault() throws Exception {
+ // a null value with a Good status is omitted; a Bad or Uncertain
status with a null value is delivered
+ template.sendBody("milo-server:node1", new
DataValue(Variant.NULL_VALUE, StatusCode.GOOD, null));
+ template.sendBody("milo-server:node2", new DataValue(new
StatusCode(StatusCodes.Uncertain_LastUsableValue)));
+
+ unknownNodeEndpoint.expectedMessageCount(2);
+ unknownNodeEndpoint.setAssertPeriod(1000);
+ context.getRouteController().startRoute("unknownNode");
+ unknownNodeEndpoint.assertIsSatisfied();
+
+ final Map<String, DataValue> values =
valuesByNode(unknownNodeEndpoint);
+ assertEquals(Set.of(UNKNOWN_NODE, NODE_2), values.keySet());
+ assertEquals(new StatusCode(StatusCodes.Bad_NodeIdUnknown),
values.get(UNKNOWN_NODE).getStatusCode());
+ assertEquals(new StatusCode(StatusCodes.Uncertain_LastUsableValue),
values.get(NODE_2).getStatusCode());
+ assertNull(values.get(NODE_2).getValue().getValue());
+
+ unknownNodeEndpoint.reset();
+ unknownNodeEndpoint.expectedMessageCount(1);
+
unknownNodeEndpoint.expectedHeaderReceived(MiloConstants.HEADER_NODE_ID,
NODE_1);
+ testBody(unknownNodeEndpoint.message(0), assertGoodValue("Baz"));
+ template.sendBody("milo-server:node1", "Baz");
+ unknownNodeEndpoint.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 739fad1e7cb8..0814687b4a00 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
@@ -1188,10 +1188,21 @@ support it keep their existing projection. The `camel
semantic audit --json` dis
base64 and reports metadata instead. Use `--output=<new-file>` to export the
full returned JSON,
with `--include-input` for case/bulk requests that need captured input.
Existing files are not overwritten.
+=== camel-milo - a Bad or Uncertain status is delivered by default
+
+A `milo-client` consumer now delivers a data change with a Bad or Uncertain
status by default, even when its value is
+null; previously the default `omitNullValues=true` dropped it.
`omitNullValues` now only omits a null value with a Good
+status. A route may therefore receive a Bad status with a null value, also as
the first notification of a monitored
+item at startup and after a reconnect, for example `Bad_WaitingForInitialData`
or the status of a `milo-server` item
+that was not written yet. A route that uses the value must check
`getStatusCode()` of the `DataValue` body first:
+Camel's type converter returns `0` or `false` for a null value that is
converted to a primitive type such as `double`
+or `boolean`. To skip the startup notification, filter on its status code; a
filter on `isGood()` would also drop the
+Bad status of a failed sensor.
+
=== camel-milo - creating and deleting the monitored items of a consumer
Since 4.16, a `milo-client` consumer whose monitored item could not be
created, for example because the node does not
-exist, received nothing. Now it receives one `DataValue` with the bad status
code (when `omitNullValues=false`). The
+exist, received nothing. Now it receives one `DataValue` with the bad status
code. The
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.
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java
index fdde075f0dce..079691e01dc6 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java
@@ -271,7 +271,8 @@ public interface MiloClientEndpointBuilderFactory {
return this;
}
/**
- * Omit notifications in case of null values.
+ * Omit 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 option is a: <code>boolean</code> type.
*
@@ -286,7 +287,8 @@ public interface MiloClientEndpointBuilderFactory {
return this;
}
/**
- * Omit notifications in case of null values.
+ * Omit 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 option will be converted to a <code>boolean</code> type.
*
@@ -1153,7 +1155,8 @@ public interface MiloClientEndpointBuilderFactory {
return this;
}
/**
- * Omit notifications in case of null values.
+ * Omit 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 option is a: <code>boolean</code> type.
*
@@ -1168,7 +1171,8 @@ public interface MiloClientEndpointBuilderFactory {
return this;
}
/**
- * Omit notifications in case of null values.
+ * Omit 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 option will be converted to a <code>boolean</code> type.
*
@@ -1949,7 +1953,8 @@ public interface MiloClientEndpointBuilderFactory {
return this;
}
/**
- * Omit notifications in case of null values.
+ * Omit 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 option is a: <code>boolean</code> type.
*
@@ -1964,7 +1969,8 @@ public interface MiloClientEndpointBuilderFactory {
return this;
}
/**
- * Omit notifications in case of null values.
+ * Omit 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 option will be converted to a <code>boolean</code> type.
*