feat(scripted-alarms): spawn + apply ScriptedAlarmHostActor in DriverHostActor (T10)
This commit is contained in:
@@ -3,6 +3,8 @@ using Akka.Actor;
|
||||
using Akka.Cluster.Tools.PublishSubscribe;
|
||||
using Akka.Event;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
|
||||
@@ -14,7 +16,10 @@ using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
||||
using CommonsNodeId = ZB.MOM.WW.OtOpcUa.Commons.Types.NodeId;
|
||||
|
||||
@@ -56,6 +61,9 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
private readonly IDriverHealthPublisher _healthPublisher;
|
||||
private readonly IVirtualTagEvaluator _virtualTagEvaluator;
|
||||
private readonly IActorRef? _virtualTagHostOverride;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ScriptRootLogger? _scriptRootLogger;
|
||||
private readonly IActorRef? _scriptedAlarmHostOverride;
|
||||
private readonly ILoggingAdapter _log = Context.GetLogger();
|
||||
|
||||
/// <summary>The single VirtualTag-host child that spawns/reconciles Equipment-namespace
|
||||
@@ -64,6 +72,14 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
/// <see cref="VirtualTagHostActor.ApplyVirtualTags"/> from <see cref="PushDesiredSubscriptions"/>.</summary>
|
||||
private IActorRef? _virtualTagHost;
|
||||
|
||||
/// <summary>The single ScriptedAlarm-host child that owns the per-node
|
||||
/// <see cref="ScriptedAlarmEngine"/>, feeds it live tag values, and bridges its emissions onto the
|
||||
/// OPC UA publish actor + the cluster <c>alerts</c> topic. Spawned in <see cref="PreStart"/>
|
||||
/// alongside the VirtualTag host (requires both an OPC UA publish actor and a
|
||||
/// <see cref="ScriptRootLogger"/>); receives <see cref="ScriptedAlarmHostActor.ApplyScriptedAlarms"/>
|
||||
/// from <see cref="PushDesiredSubscriptions"/>.</summary>
|
||||
private IActorRef? _scriptedAlarmHost;
|
||||
|
||||
private RevisionHash? _currentRevision;
|
||||
private DeploymentId? _applyingDeploymentId;
|
||||
|
||||
@@ -103,6 +119,16 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
/// VirtualTag host instead of spawning a real <see cref="VirtualTagHostActor"/> child, so tests
|
||||
/// can intercept the <see cref="VirtualTagHostActor.ApplyVirtualTags"/> message. Null in
|
||||
/// production (the real host is spawned).</param>
|
||||
/// <param name="loggerFactory">Optional logger factory used to create the
|
||||
/// <see cref="EfAlarmConditionStateStore"/>'s logger when spawning the ScriptedAlarm host;
|
||||
/// defaults to <see cref="NullLoggerFactory"/> when not provided.</param>
|
||||
/// <param name="scriptRootLogger">Optional root script logger required to spawn the ScriptedAlarm
|
||||
/// host (the engine + its script logging hang off it). When null the ScriptedAlarm host is left
|
||||
/// unspawned — the graceful dev/None-deployment path.</param>
|
||||
/// <param name="scriptedAlarmHostOverride">Test seam: when supplied, this actor is used as the
|
||||
/// ScriptedAlarm host instead of spawning a real <see cref="ScriptedAlarmHostActor"/> child, so
|
||||
/// tests can intercept the <see cref="ScriptedAlarmHostActor.ApplyScriptedAlarms"/> message. Null
|
||||
/// in production (the real host is spawned).</param>
|
||||
public static Props Props(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
||||
CommonsNodeId localNode,
|
||||
@@ -113,10 +139,14 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
IActorRef? opcUaPublishActor = null,
|
||||
IDriverHealthPublisher? healthPublisher = null,
|
||||
IVirtualTagEvaluator? virtualTagEvaluator = null,
|
||||
IActorRef? virtualTagHostOverride = null) =>
|
||||
IActorRef? virtualTagHostOverride = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
ScriptRootLogger? scriptRootLogger = null,
|
||||
IActorRef? scriptedAlarmHostOverride = null) =>
|
||||
Akka.Actor.Props.Create(() => new DriverHostActor(
|
||||
dbFactory, localNode, coordinator, driverFactory, localRoles, dependencyMux, opcUaPublishActor,
|
||||
healthPublisher, virtualTagEvaluator, virtualTagHostOverride));
|
||||
healthPublisher, virtualTagEvaluator, virtualTagHostOverride,
|
||||
loggerFactory, scriptRootLogger, scriptedAlarmHostOverride));
|
||||
|
||||
/// <summary>Initializes a new DriverHostActor with the specified dependencies.</summary>
|
||||
/// <param name="dbFactory">Database context factory for configuration database access.</param>
|
||||
@@ -131,6 +161,12 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
/// defaults to <see cref="NullVirtualTagEvaluator"/>.</param>
|
||||
/// <param name="virtualTagHostOverride">Test seam: when supplied, used as the VirtualTag host
|
||||
/// instead of spawning a real <see cref="VirtualTagHostActor"/> child.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory used to create the
|
||||
/// <see cref="EfAlarmConditionStateStore"/>'s logger; defaults to <see cref="NullLoggerFactory"/>.</param>
|
||||
/// <param name="scriptRootLogger">Optional root script logger required to spawn the ScriptedAlarm
|
||||
/// host; when null the host is left unspawned.</param>
|
||||
/// <param name="scriptedAlarmHostOverride">Test seam: when supplied, used as the ScriptedAlarm host
|
||||
/// instead of spawning a real <see cref="ScriptedAlarmHostActor"/> child.</param>
|
||||
public DriverHostActor(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
||||
CommonsNodeId localNode,
|
||||
@@ -141,7 +177,10 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
IActorRef? opcUaPublishActor = null,
|
||||
IDriverHealthPublisher? healthPublisher = null,
|
||||
IVirtualTagEvaluator? virtualTagEvaluator = null,
|
||||
IActorRef? virtualTagHostOverride = null)
|
||||
IActorRef? virtualTagHostOverride = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
ScriptRootLogger? scriptRootLogger = null,
|
||||
IActorRef? scriptedAlarmHostOverride = null)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_localNode = localNode;
|
||||
@@ -153,6 +192,9 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
_healthPublisher = healthPublisher ?? NullDriverHealthPublisher.Instance;
|
||||
_virtualTagEvaluator = virtualTagEvaluator ?? NullVirtualTagEvaluator.Instance;
|
||||
_virtualTagHostOverride = virtualTagHostOverride;
|
||||
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
|
||||
_scriptRootLogger = scriptRootLogger;
|
||||
_scriptedAlarmHostOverride = scriptedAlarmHostOverride;
|
||||
|
||||
// Default behavior is Steady — PreStart may flip to Stale or replay an orphan apply.
|
||||
Become(Steady);
|
||||
@@ -168,6 +210,9 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
// Spawn the VirtualTag host BEFORE Bootstrap so the bootstrap-restore path (which routes
|
||||
// through PushDesiredSubscriptions and Tells ApplyVirtualTags) has a live host to target.
|
||||
SpawnVirtualTagHost();
|
||||
// Same rationale for the ScriptedAlarm host — the bootstrap-restore path Tells it
|
||||
// ApplyScriptedAlarms through the same PushDesiredSubscriptions pass.
|
||||
SpawnScriptedAlarmHost();
|
||||
Bootstrap();
|
||||
}
|
||||
|
||||
@@ -199,6 +244,46 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
"virtual-tag-host");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the single <see cref="ScriptedAlarmHostActor"/> child that owns the per-node
|
||||
/// <see cref="ScriptedAlarmEngine"/>, feeds it live tag values from the dependency mux, and
|
||||
/// bridges its emissions onto the OPC UA publish actor + the cluster <c>alerts</c> topic. A
|
||||
/// test-supplied <c>scriptedAlarmHostOverride</c> short-circuits the spawn so a probe can
|
||||
/// intercept <see cref="ScriptedAlarmHostActor.ApplyScriptedAlarms"/>. The real host needs
|
||||
/// both a non-null <see cref="_opcUaPublishActor"/> (the emission sink) and a non-null
|
||||
/// <see cref="_scriptRootLogger"/> (the engine + its script logging hang off it); when either
|
||||
/// is missing (legacy ControlPlane test harnesses, dev/None deployments) the host is left
|
||||
/// null and ApplyScriptedAlarms becomes a no-op. The engine is built around a fresh
|
||||
/// <see cref="DependencyMuxTagUpstreamSource"/> + an <see cref="EfAlarmConditionStateStore"/>;
|
||||
/// the host (spawned as a child) owns + disposes the engine in its PostStop, so it stops with
|
||||
/// the driver host.
|
||||
/// </summary>
|
||||
private void SpawnScriptedAlarmHost()
|
||||
{
|
||||
if (_scriptedAlarmHostOverride is not null)
|
||||
{
|
||||
_scriptedAlarmHost = _scriptedAlarmHostOverride;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_opcUaPublishActor is null || _scriptRootLogger is null)
|
||||
{
|
||||
_log.Debug(
|
||||
"DriverHost {Node}: skipping ScriptedAlarm host spawn (no publish actor / root logger)",
|
||||
_localNode);
|
||||
return;
|
||||
}
|
||||
|
||||
var upstream = new DependencyMuxTagUpstreamSource();
|
||||
var store = new EfAlarmConditionStateStore(
|
||||
_dbFactory, _loggerFactory.CreateLogger<EfAlarmConditionStateStore>());
|
||||
var engine = new ScriptedAlarmEngine(
|
||||
upstream, store, new ScriptLoggerFactory(_scriptRootLogger.Logger), _scriptRootLogger.Logger);
|
||||
_scriptedAlarmHost = Context.ActorOf(
|
||||
ScriptedAlarmHostActor.Props(_opcUaPublishActor, _dependencyMux, upstream, engine),
|
||||
"scripted-alarm-host");
|
||||
}
|
||||
|
||||
private void Bootstrap()
|
||||
{
|
||||
// Read the most-recent NodeDeploymentState for this node; if it's Applied, jump
|
||||
@@ -535,6 +620,17 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
_log.Info("DriverHost {Node}: applied {Count} Equipment VirtualTag(s) to the VirtualTag host",
|
||||
_localNode, composition.EquipmentVirtualTags.Count);
|
||||
}
|
||||
|
||||
// Same pass for Equipment-namespace ScriptedAlarms: hand the plans to the host so it
|
||||
// (re)loads its engine + re-registers mux interest for the union of dependency refs. Covers
|
||||
// both the fresh-apply and bootstrap-restore paths (both call this method); the Stale-recovery
|
||||
// path deliberately does not, matching driver + VirtualTag recovery.
|
||||
_scriptedAlarmHost?.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(composition.EquipmentScriptedAlarms));
|
||||
if (composition.EquipmentScriptedAlarms.Count > 0)
|
||||
{
|
||||
_log.Info("DriverHost {Node}: applied {Count} Equipment ScriptedAlarm(s) to the ScriptedAlarm host",
|
||||
_localNode, composition.EquipmentScriptedAlarms.Count);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnChild(DriverInstanceSpec spec)
|
||||
|
||||
@@ -11,6 +11,7 @@ using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Health;
|
||||
@@ -90,6 +91,10 @@ public static class ServiceCollectionExtensions
|
||||
var serviceLevel = resolver.GetService<IServiceLevelPublisher>() ?? NullServiceLevelPublisher.Instance;
|
||||
var loggerFactory = resolver.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
|
||||
var healthPublisher = resolver.GetService<IDriverHealthPublisher>() ?? NullDriverHealthPublisher.Instance;
|
||||
// Root script logger backs the ScriptedAlarm host's engine + script logging. Registered in
|
||||
// Host DI inside the hasDriver block; may be absent in some role configs / test harnesses,
|
||||
// in which case the DriverHostActor gracefully skips spawning the ScriptedAlarm host.
|
||||
var scriptRootLogger = resolver.GetService<ScriptRootLogger>();
|
||||
// Production evaluator is the Host's RoslynVirtualTagEvaluator (registered as
|
||||
// IVirtualTagEvaluator); fall back to the null evaluator for test harnesses that don't
|
||||
// register one (VirtualTagActor children then evaluate to nothing).
|
||||
@@ -131,7 +136,9 @@ public static class ServiceCollectionExtensions
|
||||
dependencyMux: mux,
|
||||
opcUaPublishActor: publishActor,
|
||||
healthPublisher: healthPublisher,
|
||||
virtualTagEvaluator: virtualTagEvaluator),
|
||||
virtualTagEvaluator: virtualTagEvaluator,
|
||||
loggerFactory: loggerFactory,
|
||||
scriptRootLogger: scriptRootLogger),
|
||||
DriverHostActorName);
|
||||
registry.Register<DriverHostActorKey>(driverHost);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user