feat(driver-calc): B2-WP7 Calculation driver — IDependencyConsumer + triggers + deploy cycle gate
Adds the Calculation pseudo-driver (tags computed by C# scripts over other tags' live values) plus the host seam that feeds it: - IDependencyConsumer capability interface (Core.Abstractions) + DriverTypeNames.Calculation. - CalculationDriver : IDriver, ISubscribable, IReadable, IDependencyConsumer — change-gate + dedupe (VirtualTagActor parity), timer-group scheduler, Bad-transition + script-log emission, Good recovery; reuses the T0-4 CalculationEvaluator. - DriverHostActor spawns a DependencyConsumerMuxAdapter per dependency-consuming driver, registering its refs on the per-node DependencyMuxActor and forwarding DependencyValueChanged → OnDependencyValue; re-registers on every apply, torn down with the driver. - Factory + probe registered in DriverFactoryBootstrap (keeps the T0-3 guard green); guard test csproj references the driver so the bin-scan discovers the factory. - DeploymentArtifact injects the resolved scriptSource (scriptId → SourceCode) into a calc tag's delivered TagConfig so the host-blind driver can compile it. - DraftValidator deploy gates: CalculationScriptMissing / CalculationScriptNotFound (scriptId existence) + CalculationDependencyCycle (DependencyGraph.DetectCycles over calc→calc edges). - Tests: driver units (dep-ref extraction, change-gate, dedupe, Bad+script-log+recovery, timer, read), tag-config parsing, DraftValidator gates (self/2-cycle/cross-driver-terminal), and a Runtime integration test proving values FLOW mux → adapter → driver → calc-of-calc end-to-end. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -98,6 +98,14 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
|
||||
private readonly Dictionary<string, ChildEntry> _children = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>Per-driver <see cref="DependencyConsumerMuxAdapter"/> children — one for each spawned driver
|
||||
/// that implements <see cref="IDependencyConsumer"/> (e.g. the Calculation pseudo-driver). Each registers
|
||||
/// the driver's declared dependency refs on the per-node <see cref="_dependencyMux"/> and forwards value
|
||||
/// changes into the driver. Spawned in <see cref="SpawnChild"/>, torn down with the driver in
|
||||
/// <see cref="StopDependencyAdapter"/>, and re-registered on every apply (refs change when tags/scripts
|
||||
/// change). Empty when no mux is wired (the dev/None path) or no dependency-consuming driver is hosted.</summary>
|
||||
private readonly Dictionary<string, IActorRef> _depConsumerAdapters = new(StringComparer.Ordinal);
|
||||
|
||||
// Monotonic counter feeding the child actor-name suffix (see ActorNameFor / SpawnChild). Single-
|
||||
// threaded actor, so a plain increment is safe; it only ever grows, guaranteeing a unique name per
|
||||
// spawn so a restart's respawn never collides with the still-terminating old child.
|
||||
@@ -1372,6 +1380,12 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
foreach (var id in plan.ToStop) StopChild(id);
|
||||
foreach (var spec in plan.ToApplyDelta) ApplyChildDelta(spec);
|
||||
foreach (var spec in plan.ToSpawn) SpawnChild(spec);
|
||||
|
||||
// v3 (WP7): re-register every dependency-consumer adapter's interest on each apply — a driver's
|
||||
// declared dependency refs change when its tags/scripts change. Newly-spawned adapters already
|
||||
// registered in PreStart; this refreshes the ones an ApplyDelta kept in place. A Tell (no blocking).
|
||||
foreach (var adapter in _depConsumerAdapters.Values)
|
||||
adapter.Tell(new ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags.DependencyConsumerMuxAdapter.ReRegister());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1736,10 +1750,33 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
_children[spec.DriverInstanceId] = new ChildEntry(child, spec, stub);
|
||||
|
||||
// v3 (WP7): if the (real, non-stub) driver consumes other tags' live values, spawn a mux-adapter
|
||||
// child that registers its dependency refs on the per-node dependency mux and forwards changes
|
||||
// into the driver. Requires a wired mux (dev/None deployments have none — then calc inputs simply
|
||||
// can't flow, matching the VirtualTag dev path).
|
||||
if (!stub && driver is IDependencyConsumer consumer && _dependencyMux is not null)
|
||||
{
|
||||
var adapter = Context.ActorOf(
|
||||
ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags.DependencyConsumerMuxAdapter.Props(consumer, _dependencyMux),
|
||||
"depmux-" + actorName);
|
||||
_depConsumerAdapters[spec.DriverInstanceId] = adapter;
|
||||
_log.Info("DriverHost {Node}: wired dependency-consumer mux adapter for {Id} ({Count} ref(s))",
|
||||
_localNode, spec.DriverInstanceId, consumer.DependencyRefs.Count);
|
||||
}
|
||||
|
||||
_log.Info("DriverHost {Node}: spawned {Type} driver {Id} (stub={Stub})",
|
||||
_localNode, spec.DriverType, spec.DriverInstanceId, stub);
|
||||
}
|
||||
|
||||
/// <summary>Stops + forgets the dependency-consumer mux adapter for a driver (if any). Called wherever a
|
||||
/// driver child is stopped/respawned so a stale adapter never keeps a dead driver's refs registered.</summary>
|
||||
private void StopDependencyAdapter(string driverInstanceId)
|
||||
{
|
||||
if (_depConsumerAdapters.Remove(driverInstanceId, out var adapter))
|
||||
Context.Stop(adapter);
|
||||
}
|
||||
|
||||
private void ApplyChildDelta(DriverInstanceSpec spec)
|
||||
{
|
||||
if (!_children.TryGetValue(spec.DriverInstanceId, out var entry)) return;
|
||||
@@ -1752,6 +1789,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
private void StopChild(string driverInstanceId)
|
||||
{
|
||||
if (!_children.TryGetValue(driverInstanceId, out var entry)) return;
|
||||
StopDependencyAdapter(driverInstanceId);
|
||||
Context.Stop(entry.Actor);
|
||||
_children.Remove(driverInstanceId);
|
||||
_log.Info("DriverHost {Node}: stopped driver child {Id}", _localNode, driverInstanceId);
|
||||
@@ -1805,6 +1843,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
_localNode, msg.DriverInstanceId, msg.ActorByUserName);
|
||||
|
||||
// Stop the existing child actor — DriverInstanceActor.PostStop calls ShutdownAsync.
|
||||
StopDependencyAdapter(msg.DriverInstanceId);
|
||||
Context.Stop(entry.Actor);
|
||||
_children.Remove(msg.DriverInstanceId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user