A disabled VirtualTag (Enabled = false) is still deployed and evaluated #787

Closed
opened 2026-11-10 00:47:21 -05:00 by dohertj2 · 2 comments
Owner

Found during the lmxopcua#780 docker-dev live drill (2026-11-10).

Symptom. VirtualTag.Enabled is authorable (the AdminUI VirtualTagModal has an Enabled checkbox), but the runtime ignores it. On the docker-dev rig a VT row was set to Enabled = 0 and redeployed (deployment ad49901e); its node stayed in the address space (ns=3;s=EQ-32296cc68964/drillrunaway) and its script kept running: the runaway it carried was hard-killed twice more after the deploy and re-quarantined.

Cause (code-verified on master @ f50b29ab). Neither seam that builds the runtime plan reads the flag:

  • AddressSpaceComposer.Compose (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs, ~L618) builds EquipmentVirtualTags from every VirtualTag row.
  • DeploymentArtifact (src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs, ~L1395–1441) decodes every VT element, and EquipmentVirtualTagPlan has no Enabled field.

Only the legacy EquipmentNodeWalker filters .Where(v => v.Enabled). Scripted alarms do honour their flag (ScriptedAlarmHostActor L353, AddressSpaceApplier L1131/L1139), so the two script seams behave differently.

Fix shape. Filter disabled VTs at both seams the same way, so byte-parity holds, plus a parity test. Alternatively, remove the checkbox if "disabled" is not meant to exist. Note that a filtered VT disappears from the address space, so a ScadaBridge binding to it gets BadNodeIdUnknown. Decide whether that is wanted, or whether a disabled VT should stay as a node with a Bad status.

Found during the lmxopcua#780 docker-dev live drill (2026-11-10). **Symptom.** `VirtualTag.Enabled` is authorable (the AdminUI `VirtualTagModal` has an *Enabled* checkbox), but the runtime ignores it. On the docker-dev rig a VT row was set to `Enabled = 0` and redeployed (deployment `ad49901e`); its node stayed in the address space (`ns=3;s=EQ-32296cc68964/drillrunaway`) and its script kept running: the runaway it carried was hard-killed twice more after the deploy and re-quarantined. **Cause (code-verified on `master` @ `f50b29ab`).** Neither seam that builds the runtime plan reads the flag: - `AddressSpaceComposer.Compose` (`src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs`, ~L618) builds `EquipmentVirtualTags` from every `VirtualTag` row. - `DeploymentArtifact` (`src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs`, ~L1395–1441) decodes every VT element, and `EquipmentVirtualTagPlan` has no `Enabled` field. Only the legacy `EquipmentNodeWalker` filters `.Where(v => v.Enabled)`. Scripted alarms do honour their flag (`ScriptedAlarmHostActor` L353, `AddressSpaceApplier` L1131/L1139), so the two script seams behave differently. **Fix shape.** Filter disabled VTs at both seams the same way, so byte-parity holds, plus a parity test. Alternatively, remove the checkbox if "disabled" is not meant to exist. Note that a filtered VT disappears from the address space, so a ScadaBridge binding to it gets BadNodeIdUnknown. Decide whether that is wanted, or whether a disabled VT should stay as a node with a Bad status.
dohertj2 added the type:bugpriority:p2 labels 2026-11-10 00:47:21 -05:00
Author
Owner

Fixed in 963b0d66 (master). Decision: a disabled VirtualTag is dropped from the runtime plan at both seams (AddressSpaceComposer.Compose and DeploymentArtifact.BuildEquipmentVirtualTagPlans, byte-parity) - no node materialised, no VirtualTagActor spawned/evaluated. This matches disabled scripted alarms (not materialised) and the legacy EquipmentNodeWalker; consequence: clients bound to the node get BadNodeIdUnknown while disabled (documented in docs/VirtualTags.md). DraftValidator exempts a disabled VT from VirtualTagNoEffectiveTrigger (#783) so it cannot block a deploy. Calculation tags have no Enabled flag, so no equivalent gap; scripted alarms already honoured theirs. Tests: composer/artifact parity, deploy-level DriverHostActor test (disabled VT never reaches the VT host), validator exemption - all 3 mutation-proved (fail when src reverted). Release build 0 warnings; Runtime 993, Configuration 234, VirtualTags 16, Host 269, OpcUaServer 1027, OpcUaServer.IntegrationTests 132 all pass (AKKA_TEST_TIMEFACTOR=3).

Fixed in 963b0d66 (master). Decision: a disabled VirtualTag is dropped from the runtime plan at both seams (AddressSpaceComposer.Compose and DeploymentArtifact.BuildEquipmentVirtualTagPlans, byte-parity) - no node materialised, no VirtualTagActor spawned/evaluated. This matches disabled scripted alarms (not materialised) and the legacy EquipmentNodeWalker; consequence: clients bound to the node get BadNodeIdUnknown while disabled (documented in docs/VirtualTags.md). DraftValidator exempts a disabled VT from VirtualTagNoEffectiveTrigger (#783) so it cannot block a deploy. Calculation tags have no Enabled flag, so no equivalent gap; scripted alarms already honoured theirs. Tests: composer/artifact parity, deploy-level DriverHostActor test (disabled VT never reaches the VT host), validator exemption - all 3 mutation-proved (fail when src reverted). Release build 0 warnings; Runtime 993, Configuration 234, VirtualTags 16, Host 269, OpcUaServer 1027, OpcUaServer.IntegrationTests 132 all pass (AKKA_TEST_TIMEFACTOR=3).
Author
Owner

Owner decision applied (supersedes the drop-from-plan behaviour of 963b0d66): a disabled VirtualTag now KEEPS its node and is never evaluated. Fix: f201f16f on master.

  • EquipmentVirtualTagPlan gains Enabled (in value equality); composer and artifact decode both keep the row with Enabled=false (byte-parity test).
  • Node is materialised as normal (UNS NodeId, read-only). VirtualTagHostActor spawns no evaluator for it and publishes BadOutOfService (0x808D0000) with a null value, stamped at deploy, on every apply (survives address-space rebuild). A client bound to it (ScadaBridge) reads BadOutOfService, not BadNodeIdUnknown.
  • Writes refused (read-only node; asserted on the wire). A script depending on it gets no pushed value (VT outputs are not fed through the dependency mux), never a stale Good; documented in docs/VirtualTags.md.
  • Enabled flips are node-irrelevant to the change classifier: enabled->disabled stops the child + publishes BadOutOfService; re-enable + redeploy resumes evaluation. Validator exemption (VirtualTagNoEffectiveTrigger) and name-uniqueness counting kept.
  • Scripted alarms unchanged (disabled = not materialised).

Evidence: Release build 0 warnings; Runtime.Tests 995/0 failed, OpcUaServer.Tests 1027/0, Configuration.Tests 234/0, Host.Tests 269/0, Core.VirtualTags.Tests 16/0, full OpcUaServer.IntegrationTests 133/0 incl. new DisabledVirtualTagWireTests (real client reads BadOutOfService, write refused). Mutation-proved: applier skipping disabled nodes fails the wire test; host spawning disabled plans fails the host tests; composer re-filtering disabled fails the parity test.

Owner decision applied (supersedes the drop-from-plan behaviour of 963b0d66): a disabled VirtualTag now KEEPS its node and is never evaluated. Fix: f201f16f on master. - `EquipmentVirtualTagPlan` gains `Enabled` (in value equality); composer and artifact decode both keep the row with `Enabled=false` (byte-parity test). - Node is materialised as normal (UNS NodeId, read-only). `VirtualTagHostActor` spawns no evaluator for it and publishes `BadOutOfService` (0x808D0000) with a null value, stamped at deploy, on every apply (survives address-space rebuild). A client bound to it (ScadaBridge) reads BadOutOfService, not BadNodeIdUnknown. - Writes refused (read-only node; asserted on the wire). A script depending on it gets no pushed value (VT outputs are not fed through the dependency mux), never a stale Good; documented in docs/VirtualTags.md. - Enabled flips are node-irrelevant to the change classifier: enabled->disabled stops the child + publishes BadOutOfService; re-enable + redeploy resumes evaluation. Validator exemption (VirtualTagNoEffectiveTrigger) and name-uniqueness counting kept. - Scripted alarms unchanged (disabled = not materialised). Evidence: Release build 0 warnings; Runtime.Tests 995/0 failed, OpcUaServer.Tests 1027/0, Configuration.Tests 234/0, Host.Tests 269/0, Core.VirtualTags.Tests 16/0, full OpcUaServer.IntegrationTests 133/0 incl. new `DisabledVirtualTagWireTests` (real client reads BadOutOfService, write refused). Mutation-proved: applier skipping disabled nodes fails the wire test; host spawning disabled plans fails the host tests; composer re-filtering disabled fails the parity test.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#787