feat(triggers): runtime expression trigger evaluation for scripts and alarms
This commit is contained in:
@@ -50,6 +50,12 @@ public class AlarmActor : ReceiveActor
|
||||
private readonly string? _onTriggerScriptName;
|
||||
private readonly Script<object?>? _onTriggerCompiledScript;
|
||||
|
||||
// Expression trigger: compiled expression + the attribute snapshot it
|
||||
// evaluates against. The compiled expression is also held on the
|
||||
// ExpressionEvalConfig; this field caches it for the hot path.
|
||||
private readonly Script<object?>? _compiledTriggerExpression;
|
||||
private readonly Dictionary<string, object?> _attributeSnapshot = new();
|
||||
|
||||
// Rate of change tracking
|
||||
private readonly Queue<(DateTimeOffset Timestamp, double Value)> _rateOfChangeWindow = new();
|
||||
private readonly TimeSpan _rateOfChangeWindowDuration;
|
||||
@@ -65,6 +71,7 @@ public class AlarmActor : ReceiveActor
|
||||
SharedScriptLibrary sharedScriptLibrary,
|
||||
SiteRuntimeOptions options,
|
||||
ILogger logger,
|
||||
Script<object?>? compiledTriggerExpression = null,
|
||||
ISiteHealthCollector? healthCollector = null)
|
||||
{
|
||||
_alarmName = alarmName;
|
||||
@@ -77,6 +84,7 @@ public class AlarmActor : ReceiveActor
|
||||
_priority = alarmConfig.PriorityLevel;
|
||||
_onTriggerScriptName = alarmConfig.OnTriggerScriptCanonicalName;
|
||||
_onTriggerCompiledScript = onTriggerCompiledScript;
|
||||
_compiledTriggerExpression = compiledTriggerExpression;
|
||||
|
||||
// Parse trigger type
|
||||
_triggerType = Enum.TryParse<AlarmTriggerType>(alarmConfig.TriggerType, true, out var tt)
|
||||
@@ -126,9 +134,18 @@ public class AlarmActor : ReceiveActor
|
||||
/// </summary>
|
||||
private void HandleAttributeValueChanged(AttributeValueChanged changed)
|
||||
{
|
||||
// Only evaluate if this change is for an attribute we're monitoring
|
||||
if (!IsMonitoredAttribute(changed.AttributeName))
|
||||
// Expression triggers evaluate against a snapshot of every attribute,
|
||||
// not a single monitored attribute. Keep the snapshot current for every
|
||||
// change before the IsMonitoredAttribute gate (which does not apply).
|
||||
if (_triggerType == AlarmTriggerType.Expression)
|
||||
{
|
||||
_attributeSnapshot[changed.AttributeName] = changed.Value;
|
||||
}
|
||||
else if (!IsMonitoredAttribute(changed.AttributeName))
|
||||
{
|
||||
// Only evaluate if this change is for an attribute we're monitoring
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -143,6 +160,7 @@ public class AlarmActor : ReceiveActor
|
||||
AlarmTriggerType.ValueMatch => EvaluateValueMatch(changed.Value),
|
||||
AlarmTriggerType.RangeViolation => EvaluateRangeViolation(changed.Value),
|
||||
AlarmTriggerType.RateOfChange => EvaluateRateOfChange(changed.Value, changed.Timestamp),
|
||||
AlarmTriggerType.Expression => EvaluateExpression(),
|
||||
_ => false
|
||||
};
|
||||
|
||||
@@ -337,6 +355,22 @@ public class AlarmActor : ReceiveActor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the compiled trigger expression against the current attribute
|
||||
/// snapshot, returning the resulting bool. This bool feeds the existing
|
||||
/// binary Normal↔Active state path — the alarm is active while true. A
|
||||
/// throwing or non-bool expression is treated as false; the exception
|
||||
/// propagates to the caller's catch, which logs it and continues.
|
||||
/// </summary>
|
||||
private bool EvaluateExpression()
|
||||
{
|
||||
if (_compiledTriggerExpression == null) return false;
|
||||
|
||||
var globals = new TriggerExpressionGlobals(_attributeSnapshot);
|
||||
var state = _compiledTriggerExpression.RunAsync(globals).GetAwaiter().GetResult();
|
||||
return state.ReturnValue is bool b && b;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HiLo level evaluator: returns the most-severe matching band for the
|
||||
/// given value. Severity order checked from highest to lowest so that a
|
||||
@@ -473,6 +507,14 @@ public class AlarmActor : ReceiveActor
|
||||
HiMessage: TryReadString(root, "hiMessage"),
|
||||
HiHiMessage: TryReadString(root, "hiHiMessage")),
|
||||
|
||||
// Expression triggers have no single monitored attribute; they
|
||||
// evaluate the compiled expression (passed into the actor) over
|
||||
// the full attribute snapshot. MonitoredAttributeName is unused.
|
||||
AlarmTriggerType.Expression => new ExpressionEvalConfig(
|
||||
"",
|
||||
TryReadString(root, "expression") ?? "",
|
||||
_compiledTriggerExpression),
|
||||
|
||||
_ => new ValueMatchEvalConfig(attr, null)
|
||||
};
|
||||
}
|
||||
@@ -535,6 +577,17 @@ internal record RateOfChangeEvalConfig(
|
||||
TimeSpan WindowDuration,
|
||||
RateOfChangeDirection Direction) : AlarmEvalConfig(MonitoredAttributeName);
|
||||
|
||||
/// <summary>
|
||||
/// Expression evaluation config: a read-only boolean C# expression evaluated
|
||||
/// over the full attribute snapshot. Has no single monitored attribute
|
||||
/// (<see cref="AlarmEvalConfig.MonitoredAttributeName"/> is empty); the
|
||||
/// compiled expression is passed into the actor and cached here.
|
||||
/// </summary>
|
||||
internal record ExpressionEvalConfig(
|
||||
string MonitoredAttributeName,
|
||||
string Expression,
|
||||
Script<object?>? CompiledExpression) : AlarmEvalConfig(MonitoredAttributeName);
|
||||
|
||||
/// <summary>
|
||||
/// HiLo evaluation config: any subset of the four setpoints may be set; null
|
||||
/// means "don't evaluate that band". Per-setpoint priorities override the
|
||||
|
||||
Reference in New Issue
Block a user