A disabled VirtualTag (Enabled = false) is still deployed and evaluated #787
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found during the lmxopcua#780 docker-dev live drill (2026-11-10).
Symptom.
VirtualTag.Enabledis authorable (the AdminUIVirtualTagModalhas an Enabled checkbox), but the runtime ignores it. On the docker-dev rig a VT row was set toEnabled = 0and redeployed (deploymentad49901e); 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) buildsEquipmentVirtualTagsfrom everyVirtualTagrow.DeploymentArtifact(src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs, ~L1395–1441) decodes every VT element, andEquipmentVirtualTagPlanhas noEnabledfield.Only the legacy
EquipmentNodeWalkerfilters.Where(v => v.Enabled). Scripted alarms do honour their flag (ScriptedAlarmHostActorL353,AddressSpaceApplierL1131/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.
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).Owner decision applied (supersedes the drop-from-plan behaviour of
963b0d66): a disabled VirtualTag now KEEPS its node and is never evaluated. Fix:f201f16fon master.EquipmentVirtualTagPlangainsEnabled(in value equality); composer and artifact decode both keep the row withEnabled=false(byte-parity test).VirtualTagHostActorspawns no evaluator for it and publishesBadOutOfService(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.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.