VirtualTag → VirtualTag dependencies may never update: VT outputs don't go through DependencyMuxActor #791

Closed
opened 2026-11-10 04:49:27 -05:00 by dohertj2 · 1 comment
Owner

Found while revising #787 (f201f16f): VirtualTag outputs are not published through DependencyMuxActor, which only carries driver publishes. So a VirtualTag whose dependency list names another VirtualTag's path appears never to receive that upstream's values or status, whether the upstream is enabled or disabled (BadOutOfService).

This was read from code, not run. To do:

  • Prove or disprove it with a test: VT B depends on VT A's raw path; change A's inputs; assert B re-evaluates with A's new value, and that B sees A's BadOutOfService when A is disabled.
  • If it's real, either route VT outputs into the mux (watch for cycles; #779's ScriptStackGuard covers recursion inside one evaluation, not cross-VT feedback loops), or reject VT→VT dependencies at deploy time with a clear DraftValidator error. Document which in docs/VirtualTags.md.
  • Check ctx.GetTag() reads of a VT path inside scripts too: does a script read return the VT's current value?
Found while revising #787 (`f201f16f`): VirtualTag outputs are not published through `DependencyMuxActor`, which only carries driver publishes. So a VirtualTag whose dependency list names another VirtualTag's path appears never to receive that upstream's values or status, whether the upstream is enabled or disabled (`BadOutOfService`). This was read from code, not run. To do: - [ ] Prove or disprove it with a test: VT B depends on VT A's raw path; change A's inputs; assert B re-evaluates with A's new value, and that B sees A's `BadOutOfService` when A is disabled. - [ ] If it's real, either route VT outputs into the mux (watch for cycles; #779's `ScriptStackGuard` covers recursion inside one evaluation, not cross-VT feedback loops), or reject VT→VT dependencies at deploy time with a clear `DraftValidator` error. Document which in `docs/VirtualTags.md`. - [ ] Check `ctx.GetTag()` reads of a VT path inside scripts too: does a script read return the VT's current value?
dohertj2 added the type:bugpriority:p2 labels 2026-11-10 04:49:27 -05:00
Author
Owner

Fixed in 000d7f55 (master).

Step 1: confirmed broken. Host.Tests/Engines/VirtualTagToVirtualTagFlowTests runs a real DependencyMuxActor, a real VirtualTagHostActor and the production RoslynVirtualTagEvaluator. VT B reads VT A by its key (ctx.GetTag("EQ-…/A")). On the old master A evaluated and published 10, but B never published anything: the test timed out at line 66. VT outputs only went to their OPC UA node, and the mux only carried driver values. A VT has no raw path. Its only identity is its UNS NodeId identifier {EquipmentId}/{Name}. The AdminUI completion offered the bare leaf name, which never resolved to anything.

Step 2: chose routing, not rejection. Routing turned out to be small and contained, so there was no reason to fall back to rejecting VT→VT at deploy time.

  • VirtualTagHostActor still publishes each result to its node exactly as before, so the publish path stays single-source. It also sends every real result to the mux as VirtualTagOutput, keyed by the same string. Re-asserts are not routed. A disabled VT's BadOutOfService is routed too.
  • DependencyMuxActor pins VT outputs. They are retained even before any dependent registers, never enter the replay grace, and a driver SourceRemoved cannot evict them. When a key leaves the deployed set, VirtualTagOutputsRemoved releases it.
  • DraftValidator adds a blocking VirtualTagDependencyCycle error. It covers A→B→A, a self-read, longer loops, and loops through a Calculation tag, with {{equip}} refs resolved. Disabled VTs are not graph nodes. EquipReferenceUnresolved now names the key form when {{equip}}/<name> is actually a VT.
  • Runtime backstop: on every apply the host re-detects VT cycles and does not route the outputs of tags on one.
  • The AdminUI ScriptTagCatalog now lists VTs by their key.
  • VirtualTagNoEffectiveTrigger already counts literal reads, so a VT whose only dependency is another VT passes. That is now correct, because the upstream value actually arrives.
  • Documented in docs/VirtualTags.md.

Evidence.

  • The proving test now passes: B=11, then 15 after A's input changes, and BadOutOfService once A is disabled. Inside B's script, ctx.GetTag returns A's current value.
  • New tests: VirtualTagOutputRoutingTests (6) and DraftValidatorVirtualTagCycleTests (7).
  • Mutation-proved: each of the 8 host/mux mutations and 4 validator mutations made at least one test fail.
  • Release build: 0 warnings.
  • Test runs with AKKA_TEST_TIMEFACTOR=3, all passing: Runtime 1001, Configuration 241, Core.VirtualTags 16, Host 270, OpcUaServer.Tests 1027, OpcUaServer.IntegrationTests 133, AdminUI 1968.
Fixed in `000d7f55` (master). **Step 1: confirmed broken.** `Host.Tests/Engines/VirtualTagToVirtualTagFlowTests` runs a real `DependencyMuxActor`, a real `VirtualTagHostActor` and the production `RoslynVirtualTagEvaluator`. VT B reads VT A by its key (`ctx.GetTag("EQ-…/A")`). On the old master A evaluated and published 10, but B never published anything: the test timed out at line 66. VT outputs only went to their OPC UA node, and the mux only carried driver values. A VT has no raw path. Its only identity is its UNS NodeId identifier `{EquipmentId}/{Name}`. The AdminUI completion offered the bare leaf name, which never resolved to anything. **Step 2: chose routing, not rejection.** Routing turned out to be small and contained, so there was no reason to fall back to rejecting VT→VT at deploy time. - `VirtualTagHostActor` still publishes each result to its node exactly as before, so the publish path stays single-source. It also sends every real result to the mux as `VirtualTagOutput`, keyed by the same string. Re-asserts are not routed. A disabled VT's `BadOutOfService` is routed too. - `DependencyMuxActor` pins VT outputs. They are retained even before any dependent registers, never enter the replay grace, and a driver `SourceRemoved` cannot evict them. When a key leaves the deployed set, `VirtualTagOutputsRemoved` releases it. - `DraftValidator` adds a blocking `VirtualTagDependencyCycle` error. It covers A→B→A, a self-read, longer loops, and loops through a Calculation tag, with `{{equip}}` refs resolved. Disabled VTs are not graph nodes. `EquipReferenceUnresolved` now names the key form when `{{equip}}/<name>` is actually a VT. - Runtime backstop: on every apply the host re-detects VT cycles and does not route the outputs of tags on one. - The AdminUI `ScriptTagCatalog` now lists VTs by their key. - `VirtualTagNoEffectiveTrigger` already counts literal reads, so a VT whose only dependency is another VT passes. That is now correct, because the upstream value actually arrives. - Documented in `docs/VirtualTags.md`. **Evidence.** - The proving test now passes: B=11, then 15 after A's input changes, and BadOutOfService once A is disabled. Inside B's script, `ctx.GetTag` returns A's current value. - New tests: `VirtualTagOutputRoutingTests` (6) and `DraftValidatorVirtualTagCycleTests` (7). - Mutation-proved: each of the 8 host/mux mutations and 4 validator mutations made at least one test fail. - Release build: 0 warnings. - Test runs with `AKKA_TEST_TIMEFACTOR=3`, all passing: Runtime 1001, Configuration 241, Core.VirtualTags 16, Host 270, OpcUaServer.Tests 1027, OpcUaServer.IntegrationTests 133, AdminUI 1968.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#791