fix(vtags): respawn equipment virtualtag child on in-place plan change (H1b, stillpending §1)

This commit is contained in:
Joseph Doherty
2026-06-15 10:05:29 -04:00
parent 1dc713693a
commit ada01e1af8
2 changed files with 122 additions and 7 deletions
@@ -39,6 +39,9 @@ public sealed class VirtualTagHostActor : ReceiveActor
private readonly Dictionary<string, IActorRef> _children = new(StringComparer.Ordinal);
// vtagId -> folder-scoped OPC UA NodeId the materialiser placed the variable at.
private readonly Dictionary<string, string> _nodeIdByVtag = new(StringComparer.Ordinal);
// vtagId -> the plan its currently-spawned child was built from, so we can detect an in-place
// change (edited Expression/DependencyRefs) and stop+respawn the child to adopt it.
private readonly Dictionary<string, EquipmentVirtualTagPlan> _planByVtag = new(StringComparer.Ordinal);
/// <summary>Factory method to create Props for a VirtualTagHostActor.</summary>
/// <param name="publishActor">The OPC UA publish actor that consumes
@@ -78,6 +81,22 @@ public sealed class VirtualTagHostActor : ReceiveActor
_children.Remove(vtagId);
}
// Stop + forget children whose plan changed in place (edited Expression/DependencyRefs, or a
// toggled flag). EquipmentVirtualTagPlan has element-wise value equality, so an identical
// redeploy diffs equal and is left untouched. Forgetting the child here lets the spawn-new
// loop below recreate it from the new plan — children are auto-named (no explicit name), so a
// stop+respawn cannot hit the "actor name not unique" collision.
foreach (var p in msg.Plans)
{
if (_children.ContainsKey(p.VirtualTagId)
&& _planByVtag.TryGetValue(p.VirtualTagId, out var prev)
&& !prev.Equals(p))
{
Context.Stop(_children[p.VirtualTagId]); // PostStop unregisters the old mux interest.
_children.Remove(p.VirtualTagId);
}
}
// Rebuild the NodeId map every apply so renames (Name/FolderPath/EquipmentId changes) are
// picked up. The map only contains currently-desired vtags, so a result for a removed vtag
// finds no entry and is dropped.
@@ -87,15 +106,13 @@ public sealed class VirtualTagHostActor : ReceiveActor
_nodeIdByVtag[p.VirtualTagId] = NodeIdFor(p);
}
// Spawn children for new vtagIds only — existing children keep their mux subscriptions and
// last-value dedup state. Expression/dependency changes on an existing vtag are NOT
// re-applied here; the loader's vtags are stable, and a future enhancement can stop+respawn
// a child whose plan changed (the diff already identifies ChangedEquipmentVirtualTags).
// Spawn children for vtagIds that have no live child. This covers genuinely-new vtags AND any
// vtag whose child was just forgotten above because its plan changed in place — that child is
// 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.
foreach (var p in msg.Plans)
{
// TODO(equipment-virtualtags): when a plan's Expression/DependencyRefs change in place
// (ChangedEquipmentVirtualTags), stop+respawn the child here; today only spawn-new/stop-removed
// is handled (loader vtags are stable).
if (_children.ContainsKey(p.VirtualTagId)) continue;
// Auto-name the child: vtagIds can contain characters illegal in actor names, so let Akka
@@ -113,6 +130,14 @@ public sealed class VirtualTagHostActor : ReceiveActor
_log.Debug("VirtualTagHost: spawned child for vtag {VirtualTagId}", p.VirtualTagId);
}
// Refresh the plan map to exactly the desired set so the next apply can detect in-place
// changes against what each live child was actually built from.
_planByVtag.Clear();
foreach (var p in msg.Plans)
{
_planByVtag[p.VirtualTagId] = p;
}
_log.Debug("VirtualTagHost: applied (desired={Desired}, children={Children})",
desired.Count, _children.Count);
}