feat(triggers): runtime expression trigger evaluation for scripts and alarms
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
using Akka.Actor;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Messages.ScriptExecution;
|
||||
using ScadaLink.Commons.Messages.Streaming;
|
||||
using ScadaLink.Commons.Types.Flattening;
|
||||
using ScadaLink.HealthMonitoring;
|
||||
using ScadaLink.SiteEventLogging;
|
||||
using ScadaLink.SiteRuntime.Scripts;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -40,6 +42,12 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
private int _executionCounter;
|
||||
private readonly Commons.Types.Scripts.ScriptScope _scope;
|
||||
|
||||
// Expression trigger state: compiled expression, edge-tracking, and the
|
||||
// attribute snapshot the expression evaluates against.
|
||||
private readonly Script<object?>? _compiledTriggerExpression;
|
||||
private bool _lastExpressionResult;
|
||||
private readonly Dictionary<string, object?> _attributeSnapshot = new();
|
||||
|
||||
public ITimerScheduler Timers { get; set; } = null!;
|
||||
|
||||
public ScriptActor(
|
||||
@@ -51,6 +59,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
SharedScriptLibrary sharedScriptLibrary,
|
||||
SiteRuntimeOptions options,
|
||||
ILogger logger,
|
||||
Script<object?>? compiledTriggerExpression = null,
|
||||
ISiteHealthCollector? healthCollector = null,
|
||||
IServiceProvider? serviceProvider = null)
|
||||
{
|
||||
@@ -65,6 +74,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
_serviceProvider = serviceProvider;
|
||||
_minTimeBetweenRuns = scriptConfig.MinTimeBetweenRuns;
|
||||
_scope = scriptConfig.Scope;
|
||||
_compiledTriggerExpression = compiledTriggerExpression;
|
||||
|
||||
// Parse trigger configuration
|
||||
_triggerConfig = ParseTriggerConfig(scriptConfig.TriggerType, scriptConfig.TriggerConfiguration);
|
||||
@@ -143,10 +153,15 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles attribute value changes — triggers script if configured for value-change or conditional.
|
||||
/// Handles attribute value changes — triggers script if configured for
|
||||
/// value-change, conditional, or expression. The attribute snapshot is
|
||||
/// updated for every change before any trigger logic runs.
|
||||
/// </summary>
|
||||
private void HandleAttributeValueChanged(AttributeValueChanged changed)
|
||||
{
|
||||
// Keep the snapshot current for every change, regardless of trigger type.
|
||||
_attributeSnapshot[changed.AttributeName] = changed.Value;
|
||||
|
||||
if (_triggerConfig is ValueChangeTriggerConfig valueTrigger)
|
||||
{
|
||||
if (valueTrigger.AttributeName == changed.AttributeName)
|
||||
@@ -165,6 +180,55 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_triggerConfig is ExpressionTriggerConfig)
|
||||
{
|
||||
EvaluateExpressionTrigger();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the compiled trigger expression against the current attribute
|
||||
/// snapshot and runs the script edge-triggered — once per false→true
|
||||
/// transition. A throwing or non-bool expression is treated as false and
|
||||
/// logged as a script error; the actor never crashes.
|
||||
/// </summary>
|
||||
private void EvaluateExpressionTrigger()
|
||||
{
|
||||
if (_compiledTriggerExpression == null) return;
|
||||
|
||||
bool result;
|
||||
try
|
||||
{
|
||||
var globals = new TriggerExpressionGlobals(_attributeSnapshot);
|
||||
var state = _compiledTriggerExpression.RunAsync(globals).GetAwaiter().GetResult();
|
||||
result = state.ReturnValue is bool b && b;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogExpressionError(ex);
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (result && !_lastExpressionResult)
|
||||
{
|
||||
TrySpawnExecution(null);
|
||||
}
|
||||
|
||||
_lastExpressionResult = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a trigger-expression evaluation failure to the site event log,
|
||||
/// mirroring how ScriptExecutionActor reports script errors.
|
||||
/// </summary>
|
||||
private void LogExpressionError(Exception ex)
|
||||
{
|
||||
_healthCollector?.IncrementScriptError();
|
||||
var errorMsg = $"Trigger expression for script '{_scriptName}' on instance '{_instanceName}' failed: {ex.Message}";
|
||||
_logger.LogError(ex, "Trigger expression evaluation failed: {Script} on {Instance}", _scriptName, _instanceName);
|
||||
|
||||
_ = _serviceProvider?.GetService<ISiteEventLogger>()?.LogEventAsync(
|
||||
"script", "Error", _instanceName, $"ScriptActor:{_scriptName}", errorMsg, ex.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -264,11 +328,26 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
"interval" => ParseIntervalTrigger(triggerConfigJson),
|
||||
"valuechange" => ParseValueChangeTrigger(triggerConfigJson),
|
||||
"conditional" => ParseConditionalTrigger(triggerConfigJson),
|
||||
"expression" => ParseExpressionTrigger(triggerConfigJson),
|
||||
"call" => null, // No automatic trigger — invoked only via Instance.CallScript()
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static ExpressionTriggerConfig? ParseExpressionTrigger(string? json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json)) return null;
|
||||
try
|
||||
{
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var expr = doc.RootElement.TryGetProperty("expression", out var e)
|
||||
? e.GetString()
|
||||
: null;
|
||||
return string.IsNullOrWhiteSpace(expr) ? null : new ExpressionTriggerConfig(expr);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static IntervalTriggerConfig? ParseIntervalTrigger(string? json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json)) return null;
|
||||
@@ -323,4 +402,5 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
internal record IntervalTriggerConfig(TimeSpan Interval) : ScriptTriggerConfig;
|
||||
internal record ValueChangeTriggerConfig(string AttributeName) : ScriptTriggerConfig;
|
||||
internal record ConditionalTriggerConfig(string AttributeName, string Operator, double Threshold) : ScriptTriggerConfig;
|
||||
internal record ExpressionTriggerConfig(string Expression) : ScriptTriggerConfig;
|
||||
internal abstract record ScriptTriggerConfig;
|
||||
|
||||
Reference in New Issue
Block a user