fix(triggers): bound expression evaluation, align AlarmActor error handling, dedupe config parsing
This commit is contained in:
@@ -51,8 +51,8 @@ public class AlarmActor : ReceiveActor
|
||||
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.
|
||||
// evaluates against. This field is the single home for the compiled
|
||||
// expression on the hot path.
|
||||
private readonly Script<object?>? _compiledTriggerExpression;
|
||||
private readonly Dictionary<string, object?> _attributeSnapshot = new();
|
||||
|
||||
@@ -369,16 +369,38 @@ public class AlarmActor : ReceiveActor
|
||||
/// 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.
|
||||
/// throwing, non-bool, or timed-out expression is treated as false (logged
|
||||
/// as an alarm error) so that the state machine still runs — an Active
|
||||
/// alarm correctly clears if the expression starts throwing.
|
||||
/// </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;
|
||||
try
|
||||
{
|
||||
var globals = new TriggerExpressionGlobals(_attributeSnapshot);
|
||||
// Bound evaluation with a short timeout. The CancellationToken
|
||||
// covers cooperative/async cases; a pathological CPU-bound
|
||||
// expression is not fully interruptible. Acceptable because
|
||||
// trigger expressions are authored by trusted Design-role users
|
||||
// and are compile-checked pre-deployment.
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||
var state = _compiledTriggerExpression
|
||||
.RunAsync(globals, cancellationToken: cts.Token)
|
||||
.GetAwaiter().GetResult();
|
||||
return state.ReturnValue is bool b && b;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// OperationCanceledException (timeout) falls through here too,
|
||||
// and is correctly treated as false.
|
||||
_healthCollector?.IncrementAlarmError();
|
||||
_logger.LogError(ex,
|
||||
"Alarm {Alarm} trigger expression evaluation failed on {Instance}; treated as false",
|
||||
_alarmName, _instanceName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -518,12 +540,12 @@ public class AlarmActor : ReceiveActor
|
||||
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.
|
||||
// evaluate the compiled expression (passed into the actor and
|
||||
// cached in _compiledTriggerExpression) over the full attribute
|
||||
// snapshot. MonitoredAttributeName is unused.
|
||||
AlarmTriggerType.Expression => new ExpressionEvalConfig(
|
||||
"",
|
||||
TryReadString(root, "expression") ?? "",
|
||||
_compiledTriggerExpression),
|
||||
TriggerExpressionGlobals.ExtractExpression(triggerConfigJson) ?? ""),
|
||||
|
||||
_ => new ValueMatchEvalConfig(attr, null)
|
||||
};
|
||||
@@ -590,13 +612,13 @@ internal record RateOfChangeEvalConfig(
|
||||
/// <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.
|
||||
/// (<see cref="AlarmEvalConfig.MonitoredAttributeName"/> is empty). The
|
||||
/// compiled expression itself lives on the actor's <c>_compiledTriggerExpression</c>
|
||||
/// field, the single source for the hot path.
|
||||
/// </summary>
|
||||
internal record ExpressionEvalConfig(
|
||||
string MonitoredAttributeName,
|
||||
string Expression,
|
||||
Script<object?>? CompiledExpression) : AlarmEvalConfig(MonitoredAttributeName);
|
||||
string Expression) : AlarmEvalConfig(MonitoredAttributeName);
|
||||
|
||||
/// <summary>
|
||||
/// HiLo evaluation config: any subset of the four setpoints may be set; null
|
||||
|
||||
Reference in New Issue
Block a user