fix(v3-batch4): Equipment VirtualTag runtime dependency/publish resolution (live-gate)
Equipment VirtualTags published Bad at runtime whenever their dependency
value was static. Root cause: a deploy re-materialises each VirtualTag's
UNS node to BadWaitingForInitialData (OpcUaPublishActor.HandleRebuild), but
a surviving unchanged-plan VirtualTagActor keeps its value-dedup state, so
its unchanged recompute is suppressed (VirtualTagActor.OnDependencyChanged
value dedup) and the freshly-reset node stays Bad forever. Only masked when
the dependency value changes every poll.
Fix: VirtualTagHostActor tells each surviving (not just-spawned) child to
ReassertValue on every apply; the child re-emits its last value/quality,
bypassing dedup. Ordering is safe — DriverHostActor enqueues the
RebuildAddressSpace (materialise) to the single-threaded publish actor
before the ApplyVirtualTags that triggers the re-assert, so the re-publish
lands on the freshly-materialised node.
Regression test: VirtualTagHostActorTests
.Unchanged_redeploy_reasserts_last_value_so_a_reset_uns_node_recovers
(fails before, passes after). Live-verified on docker-dev: GateVt reads
Good via both the absolute and the {{equip}}/MainPressure script forms.
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -31,6 +31,13 @@ public sealed class VirtualTagActor : ReceiveActor
|
||||
|
||||
public sealed record DependencyValueChanged(string TagId, object? Value, DateTime TimestampUtc);
|
||||
|
||||
/// <summary>Re-assert the last emitted value/quality to the parent bridge, bypassing the value dedup.
|
||||
/// Sent by <see cref="VirtualTagHostActor"/> on every apply (deploy) to a surviving child: a deploy
|
||||
/// re-materialises the child's published UNS node to <c>BadWaitingForInitialData</c>, but an
|
||||
/// unchanged-plan child keeps its dedup state, so a static-dependency recompute is suppressed and the
|
||||
/// node would stay Bad forever. Re-asserting the last state recovers it. Singleton — carries no data.</summary>
|
||||
public sealed class ReassertValue { public static readonly ReassertValue Instance = new(); private ReassertValue() { } }
|
||||
|
||||
/// <summary>Result emitted to the parent (the host bridge). <paramref name="Quality"/> carries the
|
||||
/// OPC UA quality the node should read: <see cref="OpcUaQuality.Good"/> for a fresh computed value,
|
||||
/// <see cref="OpcUaQuality.Bad"/> when the script failed/timed out (02/S13) — in which case
|
||||
@@ -105,6 +112,7 @@ public sealed class VirtualTagActor : ReceiveActor
|
||||
_mux = mux;
|
||||
|
||||
Receive<DependencyValueChanged>(OnDependencyChanged);
|
||||
Receive<ReassertValue>(_ => OnReassertValue());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -122,6 +130,21 @@ public sealed class VirtualTagActor : ReceiveActor
|
||||
_mux?.Tell(new DependencyMuxActor.UnregisterInterest(Self));
|
||||
}
|
||||
|
||||
/// <summary>Re-emit the last emitted value/quality to the parent bridge, bypassing the value dedup, so a
|
||||
/// deploy that re-materialised our published UNS node to <c>BadWaitingForInitialData</c> recovers even when
|
||||
/// the recomputed value is unchanged (permanent Bad otherwise, with a static dependency). No-op until the
|
||||
/// child has emitted at least once — the first dependency arrival publishes the initial value normally.
|
||||
/// Ordering is safe: <c>DriverHostActor</c> enqueues the address-space <c>RebuildAddressSpace</c> (which
|
||||
/// re-materialises the node) to the single-threaded publish actor BEFORE the <c>ApplyVirtualTags</c> that
|
||||
/// triggers this re-assert, so the re-publish lands on the freshly-materialised node.</summary>
|
||||
private void OnReassertValue()
|
||||
{
|
||||
if (!_hasLastValue && !_lastPublishedBad) return;
|
||||
var quality = _lastPublishedBad ? OpcUaQuality.Bad : OpcUaQuality.Good;
|
||||
Context.Parent.Tell(new EvaluationResult(
|
||||
_virtualTagId, _lastValue, DateTime.UtcNow, CorrelationId.NewId(), quality));
|
||||
}
|
||||
|
||||
private void OnDependencyChanged(DependencyValueChanged msg)
|
||||
{
|
||||
_dependencies[msg.TagId] = msg.Value;
|
||||
|
||||
@@ -150,6 +150,9 @@ public sealed class VirtualTagHostActor : ReceiveActor
|
||||
// recreated here from the new plan, adopting the edited Expression/DependencyRefs. Children
|
||||
// whose plan was unchanged still have a live entry and are skipped, keeping their mux
|
||||
// subscriptions and last-value dedup state.
|
||||
// Track the just-spawned vtags so the re-assert pass below skips them: a fresh child has no last
|
||||
// value to re-assert and publishes its initial value on the first dependency arrival anyway.
|
||||
var newlySpawned = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var p in msg.Plans)
|
||||
{
|
||||
if (_children.ContainsKey(p.VirtualTagId)) continue;
|
||||
@@ -166,6 +169,7 @@ public sealed class VirtualTagHostActor : ReceiveActor
|
||||
mux: _mux));
|
||||
Context.Watch(child);
|
||||
_children[p.VirtualTagId] = child;
|
||||
newlySpawned.Add(p.VirtualTagId);
|
||||
_log.Debug("VirtualTagHost: spawned child for vtag {VirtualTagId}", p.VirtualTagId);
|
||||
}
|
||||
|
||||
@@ -177,6 +181,20 @@ public sealed class VirtualTagHostActor : ReceiveActor
|
||||
_planByVtag[p.VirtualTagId] = p;
|
||||
}
|
||||
|
||||
// Re-assert last values on surviving (not just-spawned) children. Every apply coincides with a
|
||||
// deploy, and DriverHostActor enqueues the address-space RebuildAddressSpace — which re-materialises
|
||||
// each VirtualTag's UNS node to BadWaitingForInitialData — to the single-threaded publish actor
|
||||
// BEFORE this ApplyVirtualTags. A surviving child keeps its value-dedup state, so an unchanged
|
||||
// recompute is suppressed and its freshly-reset node would stay Bad forever (permanent with a static
|
||||
// dependency). Telling each survivor to ReassertValue re-publishes its last state through the same
|
||||
// publish actor AFTER the materialise, so the node recovers. Fresh children are skipped (nothing to
|
||||
// re-assert; their first dependency arrival publishes normally).
|
||||
foreach (var (vtagId, child) in _children)
|
||||
{
|
||||
if (newlySpawned.Contains(vtagId)) continue;
|
||||
child.Tell(VirtualTagActor.ReassertValue.Instance);
|
||||
}
|
||||
|
||||
_log.Debug("VirtualTagHost: applied (desired={Desired}, children={Children})",
|
||||
desired.Count, _children.Count);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user