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 8b3ac0766cbbf..8b32e5c1d6ffc 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": "Monitor Filter Type for MonitoredItems." }, "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 e04f72f92737f..046501c41d7d6 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 8b3ac0766cbbf..8b32e5c1d6ffc 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": "Monitor Filter Type for MonitoredItems." }, "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 e04f72f92737f..046501c41d7d6 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 a6b6e0c2cd1c1..59e0c90aabc18 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 @@ protected void doStop() throws Exception { 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 @@ private void handleValueUpdate(final String node, final DataValue value) { } } + /** + * 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 752e4adb114d7..afc7cc34ec2fe 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 b5f50b2416913..cec8822d6eb0f 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 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 void configure() { 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 void configure() { @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 @@ void unknownNodeReceivesBadStatus() throws Exception { 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 a673c6d3bf0e6..152582e3bb009 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 @@ protected RoutesBuilder createRouteBuilder() { 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 void testMonitorItem1() throws Exception { 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 07509dea2348e..3181f2457a35e 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 @@ protected RoutesBuilder createRouteBuilder() { 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 void testMonitorItem1() throws Exception { /* * 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 void testMonitorItem1() throws Exception { 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 void testMonitorItem1() throws Exception { 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 18460c74164ab..cef04fa5f63ce 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.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 @@ void unknownNodeReceivesBadStatusAndTheOtherNodesKeepWorking() throws Exception 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 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 ebcf624762ff9..6df4997cfa8b5 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 @@ -863,10 +863,21 @@ support it keep their existing projection. The `camel semantic audit --json` dis base64 and reports metadata instead. Use `--output=` 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 fdde075f0dce3..079691e01dc67 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 @@ default MiloClientEndpointConsumerBuilder node(String node) { 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: boolean type. * @@ -286,7 +287,8 @@ default MiloClientEndpointConsumerBuilder omitNullValues(boolean omitNullValues) 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 boolean type. * @@ -1153,7 +1155,8 @@ default MiloClientEndpointProducerBuilder node(String node) { 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: boolean type. * @@ -1168,7 +1171,8 @@ default MiloClientEndpointProducerBuilder omitNullValues(boolean omitNullValues) 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 boolean type. * @@ -1949,7 +1953,8 @@ default MiloClientEndpointBuilder node(String node) { 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: boolean type. * @@ -1964,7 +1969,8 @@ default MiloClientEndpointBuilder omitNullValues(boolean omitNullValues) { 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 boolean type. *