From bf1624d5593aa3016054fea09e4f03dbba3248d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serdar=20G=C3=B6kay?= Date: Thu, 15 Oct 2026 18:37:07 +0300 Subject: [PATCH 1/3] CAMEL-25626: camel-milo - deliver a data change with a Bad status even 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 Co-authored-by: Claude Fable 5.1 --- .../camel/catalog/components/milo-client.json | 2 +- .../catalog/docs/milo-client-component.adoc | 5 ++-- .../component/milo/client/milo-client.json | 2 +- .../src/main/docs/milo-client-component.adoc | 5 ++-- .../milo/client/MiloClientConsumer.java | 18 +++++++++-- .../milo/client/MiloClientEndpoint.java | 3 +- .../milo/MonitorItemLifecycleTest.java | 8 ++--- .../component/milo/MonitorNodesTest.java | 30 ++++++++++++++++++- .../pages/camel-4x-upgrade-guide-4_24.adoc | 6 +++- .../dsl/MiloClientEndpointBuilderFactory.java | 24 +++++++++++---- 10 files changed, 81 insertions(+), 22 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 8b3ac0766cbbf..a903907f528f8 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, for example the initial value at startup. 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..35e22426ffb3d 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,9 @@ 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. +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, for example the initial value at startup. === 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..a903907f528f8 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, for example the initial value at startup. 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..35e22426ffb3d 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,9 @@ 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. +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, for example the initial value at startup. === 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..f0fb133e44415 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 @@ -30,6 +30,7 @@ import org.apache.camel.support.DefaultConsumer; 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -116,8 +117,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 +132,19 @@ private void handleValueUpdate(final String node, final DataValue value) { } } + /** + * A null value with a Good status is the initial value at startup, which omitNullValues hides. A null value with a + * Bad or Uncertain status carries the quality of the node and is delivered. + */ + private static boolean isNullValueWithGoodStatus(final DataValue value) { + if (value == null) { + return true; + } + final boolean nullValue = value.getValue() == null || value.getValue().getValue() == null; + final StatusCode status = value.getStatusCode(); + return nullValue && (status == null || status.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..26aacd86ccd38 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, for example the initial value at startup. 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..185687d2afe76 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 @@ -77,15 +77,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"; 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..b1de9d254070e 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,32 @@ void unknownNodeReceivesBadStatusAndTheOtherNodesKeepWorking() throws Exception unknownNodeEndpoint.assertIsSatisfied(); } + @Test + void notGoodStatusWithNullValueIsDeliveredByDefault() throws Exception { + // a null value with a Good status, like the initial value at startup, 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..f2f0c25278fbe 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 @@ -866,10 +866,14 @@ with `--include-input` for case/bulk requests that need captured input. Existing === 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. +A data change with a Bad or Uncertain status is now delivered by default, even when its value is null; previously the +default `omitNullValues=true` dropped it, so the route did not learn that a node went bad. `omitNullValues` now only +omits a data change whose value is null and whose status is Good, for example the initial value at startup. + The consumer now sets the `CamelMiloNodeId` header on every exchange, including a consumer with a single `node`. Stopping a consumer now deletes its monitored items. Previously they stayed on the server while another endpoint kept 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..99d10109b6894 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,9 @@ 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, for + * example the initial value at startup. A data change with a Bad or + * Uncertain status is always delivered. * * The option is a: boolean type. * @@ -286,7 +288,9 @@ 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, for + * example the initial value at startup. A data change with a Bad or + * Uncertain status is always delivered. * * The option will be converted to a boolean type. * @@ -1153,7 +1157,9 @@ 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, for + * example the initial value at startup. A data change with a Bad or + * Uncertain status is always delivered. * * The option is a: boolean type. * @@ -1168,7 +1174,9 @@ 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, for + * example the initial value at startup. A data change with a Bad or + * Uncertain status is always delivered. * * The option will be converted to a boolean type. * @@ -1949,7 +1957,9 @@ 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, for + * example the initial value at startup. A data change with a Bad or + * Uncertain status is always delivered. * * The option is a: boolean type. * @@ -1964,7 +1974,9 @@ 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, for + * example the initial value at startup. A data change with a Bad or + * Uncertain status is always delivered. * * The option will be converted to a boolean type. * From 891bf7612420818a25d43fafb97b289549af27c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serdar=20G=C3=B6kay?= Date: Thu, 15 Oct 2026 19:06:51 +0300 Subject: [PATCH 2/3] CAMEL-25626: camel-milo - follow Milo's nullability contract in the omitNullValues 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 --- .../camel/component/milo/client/MiloClientConsumer.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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 f0fb133e44415..4ffe1d084a1bd 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 @@ -30,7 +30,6 @@ import org.apache.camel.support.DefaultConsumer; 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -137,12 +136,8 @@ private void handleValueUpdate(final String node, final DataValue value) { * Bad or Uncertain status carries the quality of the node and is delivered. */ private static boolean isNullValueWithGoodStatus(final DataValue value) { - if (value == null) { - return true; - } - final boolean nullValue = value.getValue() == null || value.getValue().getValue() == null; - final StatusCode status = value.getStatusCode(); - return nullValue && (status == null || status.isGood()); + return value == null + || (value.getValue().getValue() == null && value.getStatusCode().isGood()); } private void mapToMessage(final DataValue value, final Message message) { From 23f226f00ade84172ee67670861b624c25f13f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serdar=20G=C3=B6kay?= Date: Thu, 15 Oct 2026 22:38:30 +0300 Subject: [PATCH 3/3] CAMEL-25626: camel-milo - document and test the Bad status at startup 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 Co-authored-by: Claude Fable 5.1 --- .../camel/catalog/components/milo-client.json | 2 +- .../catalog/docs/milo-client-component.adoc | 13 ++++++- .../component/milo/client/milo-client.json | 2 +- .../src/main/docs/milo-client-component.adoc | 13 ++++++- .../milo/client/MiloClientConsumer.java | 4 +- .../milo/client/MiloClientEndpoint.java | 4 +- .../milo/MonitorItemLifecycleTest.java | 31 +++++++++++++-- .../milo/MonitorItemMultiConnectionsTest.java | 10 +++-- .../camel/component/milo/MonitorItemTest.java | 38 ++++++++++--------- .../component/milo/MonitorNodesTest.java | 3 +- .../pages/camel-4x-upgrade-guide-4_24.adoc | 15 ++++++-- .../dsl/MiloClientEndpointBuilderFactory.java | 30 ++++++--------- 12 files changed, 108 insertions(+), 57 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 a903907f528f8..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 a data change whose value is null and whose status is Good, for example the initial value at startup. A data change with a Bad or Uncertain status is always delivered." }, + "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 35e22426ffb3d..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 @@ -150,8 +150,19 @@ 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. 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, for example the initial value at startup. +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 a903907f528f8..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 a data change whose value is null and whose status is Good, for example the initial value at startup. A data change with a Bad or Uncertain status is always delivered." }, + "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 35e22426ffb3d..046501c41d7d6 100644 --- a/components/camel-milo/src/main/docs/milo-client-component.adoc +++ b/components/camel-milo/src/main/docs/milo-client-component.adoc @@ -150,8 +150,19 @@ 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. 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, for example the initial value at startup. +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 4ffe1d084a1bd..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 @@ -132,8 +132,8 @@ private void handleValueUpdate(final String node, final DataValue value) { } /** - * A null value with a Good status is the initial value at startup, which omitNullValues hides. A null value with a - * Bad or Uncertain status carries the quality of the node and is delivered. + * 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 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 26aacd86ccd38..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,8 +91,8 @@ public class MiloClientEndpoint extends DefaultEndpoint { private boolean defaultAwaitWrites; /** - * Omit a data change whose value is null and whose status is Good, for example the initial value at startup. A data - * change with a Bad or Uncertain status is always delivered. + * 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 185687d2afe76..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; /** @@ -124,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); @@ -143,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 @@ -178,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 b1de9d254070e..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 @@ -148,8 +148,7 @@ void unknownNodeReceivesBadStatusAndTheOtherNodesKeepWorking() throws Exception @Test void notGoodStatusWithNullValueIsDeliveredByDefault() throws Exception { - // a null value with a Good status, like the initial value at startup, is omitted; a Bad or Uncertain status - // with a null value is delivered + // 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))); 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 f2f0c25278fbe..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,6 +863,17 @@ 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 @@ -870,10 +881,6 @@ exist, received nothing. Now it receives one `DataValue` with the bad status cod monitored item is created again when the route is restarted or when Camel rebuilds the connection to the server; unlike Camel 3.x, it is not retried every 10 seconds. -A data change with a Bad or Uncertain status is now delivered by default, even when its value is null; previously the -default `omitNullValues=true` dropped it, so the route did not learn that a node went bad. `omitNullValues` now only -omits a data change whose value is null and whose status is Good, for example the initial value at startup. - The consumer now sets the `CamelMiloNodeId` header on every exchange, including a consumer with a single `node`. Stopping a consumer now deletes its monitored items. Previously they stayed on the server while another endpoint kept 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 99d10109b6894..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,9 +271,8 @@ default MiloClientEndpointConsumerBuilder node(String node) { return this; } /** - * Omit a data change whose value is null and whose status is Good, for - * example the initial value at startup. A data change with a Bad or - * Uncertain status is always delivered. + * 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. * @@ -288,9 +287,8 @@ default MiloClientEndpointConsumerBuilder omitNullValues(boolean omitNullValues) return this; } /** - * Omit a data change whose value is null and whose status is Good, for - * example the initial value at startup. A data change with a Bad or - * Uncertain status is always delivered. + * 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. * @@ -1157,9 +1155,8 @@ default MiloClientEndpointProducerBuilder node(String node) { return this; } /** - * Omit a data change whose value is null and whose status is Good, for - * example the initial value at startup. A data change with a Bad or - * Uncertain status is always delivered. + * 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. * @@ -1174,9 +1171,8 @@ default MiloClientEndpointProducerBuilder omitNullValues(boolean omitNullValues) return this; } /** - * Omit a data change whose value is null and whose status is Good, for - * example the initial value at startup. A data change with a Bad or - * Uncertain status is always delivered. + * 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. * @@ -1957,9 +1953,8 @@ default MiloClientEndpointBuilder node(String node) { return this; } /** - * Omit a data change whose value is null and whose status is Good, for - * example the initial value at startup. A data change with a Bad or - * Uncertain status is always delivered. + * 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. * @@ -1974,9 +1969,8 @@ default MiloClientEndpointBuilder omitNullValues(boolean omitNullValues) { return this; } /** - * Omit a data change whose value is null and whose status is Good, for - * example the initial value at startup. A data change with a Bad or - * Uncertain status is always delivered. + * 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. *