using Akka.Actor; using Microsoft.CodeAnalysis.Scripting; using Microsoft.Extensions.Logging; using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors; /// /// Alarm Execution Actor -- short-lived child of Alarm Actor. /// Same pattern as ScriptExecutionActor. /// CAN call Instance.CallScript() (ask to sibling Script Actor). /// Instance scripts CANNOT call alarm on-trigger scripts (no API for it). /// Supervision: Stop on unhandled exception. /// public class AlarmExecutionActor : ReceiveActor { /// Initializes a new and immediately schedules execution of the alarm on-trigger script. /// The canonical name of the alarm that triggered. /// The name of the owning instance. /// The alarm severity level at the time of triggering. /// The alarm priority value. /// The alarm message to pass to the script. /// The pre-compiled on-trigger script to execute. /// Reference to the parent instance actor for attribute/script calls. /// Shared script library providing common utilities. /// Site runtime configuration options, including the execution timeout. /// Logger for execution diagnostics. /// The on-trigger script's per-script execution timeout in seconds. Null or non-positive falls back to the global . /// /// ParentExecutionId tag-cascade: the execution id of /// the context that fired this alarm, threaded into the on-trigger script's /// as its ParentExecutionId so the /// alarm-triggered run chains under its firing context. Null today (no /// Guid-typed firing id exists yet) — the run is a root, but the plumbing /// is in place for a future firing id. /// public AlarmExecutionActor( string alarmName, string instanceName, AlarmLevel level, int priority, string message, Script compiledScript, IActorRef instanceActor, SharedScriptLibrary sharedScriptLibrary, SiteRuntimeOptions options, ILogger logger, // Per-script execution timeout override (seconds) for the // alarm on-trigger script. Null or non-positive falls back to the global. int? executionTimeoutSeconds = null, // The firing context's execution id (null today). Guid? parentExecutionId = null) { var self = Self; var parent = Context.Parent; ExecuteAlarmScript( alarmName, instanceName, level, priority, message, compiledScript, instanceActor, sharedScriptLibrary, options, self, parent, logger, executionTimeoutSeconds, parentExecutionId); } private static void ExecuteAlarmScript( string alarmName, string instanceName, AlarmLevel level, int priority, string message, Script compiledScript, IActorRef instanceActor, SharedScriptLibrary sharedScriptLibrary, SiteRuntimeOptions options, IActorRef self, IActorRef parent, ILogger logger, int? executionTimeoutSeconds, Guid? parentExecutionId) { // Per-script timeout overrides the global default. A null or // non-positive per-script value (≤ 0) falls back to the global. var timeout = TimeSpan.FromSeconds( executionTimeoutSeconds is { } perScript && perScript > 0 ? perScript : options.ScriptExecutionTimeoutSeconds); // Run the alarm on-trigger body on the dedicated // script-execution scheduler, not the shared .NET thread pool. var scheduler = ScriptExecutionScheduler.Shared(options); _ = Task.Factory.StartNew(async () => { using var cts = new CancellationTokenSource(timeout); try { // AlarmExecutionActor can call Instance.CallScript() // via the ScriptRuntimeContext injected into globals var context = new ScriptRuntimeContext( instanceActor, self, sharedScriptLibrary, currentCallDepth: 0, options.MaxScriptCallDepth, timeout, instanceName, logger, // ParentExecutionId tag-cascade: the // alarm on-trigger run mints its own fresh ExecutionId (the // ctor's `?? NewGuid()` fallback) and records the firing // context's id as its ParentExecutionId — null today, so the // run is a root, but the plumbing exists for a future // firing id. parentExecutionId: parentExecutionId, // WaitForAttribute (spec §4.4): thread the alarm on-trigger // script's per-script execution-timeout token so a // Attributes.WaitAsync inside an on-trigger script is bounded // by the same script deadline. scriptTimeoutToken: cts.Token); var globals = new ScriptGlobals { Instance = context, Parameters = new ScriptParameters(), CancellationToken = cts.Token, Alarm = new AlarmContext { Name = alarmName, Level = level, Priority = priority, Message = message } }; await compiledScript.RunAsync(globals, cts.Token); parent.Tell(new AlarmActor.AlarmExecutionCompleted(alarmName, true)); } catch (OperationCanceledException) { logger.LogWarning( "Alarm on-trigger script for {Alarm} on {Instance} timed out", alarmName, instanceName); parent.Tell(new AlarmActor.AlarmExecutionCompleted(alarmName, false)); } catch (Exception ex) { // Failures logged, alarm continues logger.LogError(ex, "Alarm on-trigger script for {Alarm} on {Instance} failed", alarmName, instanceName); parent.Tell(new AlarmActor.AlarmExecutionCompleted(alarmName, false)); } finally { self.Tell(PoisonPill.Instance); } }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, scheduler).Unwrap(); } }