fix(core-scripted-alarms): resolve Low code-review findings (Core.ScriptedAlarms-003,006,008,010,011; -009 documented)
- Core.ScriptedAlarms-003: emit OnEvent OUTSIDE _evalGate by collecting
pending emissions during the gate-held section and flushing them after
release; eliminates re-entrancy deadlock the docs already promised.
- Core.ScriptedAlarms-006: track every fire-and-forget Reevaluate /
ShelvingCheck task in _inFlight; Dispose drains the set so the engine
no longer races store writes against teardown.
- Core.ScriptedAlarms-008: store comments as ImmutableList<AlarmComment>
so AppendComment is O(log n) instead of O(n).
- Core.ScriptedAlarms-010: document the deliberate input-quality
asymmetry (Uncertain drives the predicate, renders {?} in the message)
in docs/ScriptedAlarms.md and on MessageTemplate.Resolve remarks.
- Core.ScriptedAlarms-011: propagate the no-op reason through
TransitionResult.NoOp(state, reason) and log it from
ScriptedAlarmEngine.ApplyAsync.
- Core.ScriptedAlarms-009 (Won't Fix per recommendation): documented the
per-evaluation dictionary allocation in docs/v2/Galaxy.Performance.md
with a mitigation path if a future soak surfaces pressure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
||||
|
||||
/// <summary>
|
||||
@@ -258,21 +260,33 @@ public static class Part9StateMachine
|
||||
return s.UnshelveAtUtc is DateTime t && nowUtc >= t ? ShelvingState.Unshelved : s;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AlarmComment> AppendComment(
|
||||
IReadOnlyList<AlarmComment> existing, DateTime ts, string user, string kind, string? text)
|
||||
{
|
||||
var list = new List<AlarmComment>(existing.Count + 1);
|
||||
list.AddRange(existing);
|
||||
list.Add(new AlarmComment(ts, user, kind, text ?? string.Empty));
|
||||
return list;
|
||||
}
|
||||
private static ImmutableList<AlarmComment> AppendComment(
|
||||
ImmutableList<AlarmComment> existing, DateTime ts, string user, string kind, string? text)
|
||||
=> existing.Add(new AlarmComment(ts, user, kind, text ?? string.Empty));
|
||||
}
|
||||
|
||||
/// <summary>Result of a state-machine operation — new state + what to emit (if anything).</summary>
|
||||
public sealed record TransitionResult(AlarmConditionState State, EmissionKind Emission)
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="NoOpReason"/> carries a short diagnostic string for the
|
||||
/// <see cref="NoOp(AlarmConditionState, string)"/> case (e.g.
|
||||
/// "disabled — predicate result ignored", "already acknowledged"). The
|
||||
/// engine logs this at debug level when a no-op result is observed, so
|
||||
/// the class-level remarks on <see cref="Part9StateMachine"/> hold:
|
||||
/// disabled-alarm and idempotent ack/confirm/shelve/unshelve
|
||||
/// transitions do produce a diagnostic log line. Plain
|
||||
/// <see cref="None(AlarmConditionState)"/> results (state unchanged,
|
||||
/// no operator intent recorded — e.g. a predicate re-evaluation that
|
||||
/// confirms the existing active state) leave <see cref="NoOpReason"/>
|
||||
/// null because there is nothing to surface to an operator.
|
||||
/// (Core.ScriptedAlarms-011)
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed record TransitionResult(AlarmConditionState State, EmissionKind Emission, string? NoOpReason = null)
|
||||
{
|
||||
public static TransitionResult None(AlarmConditionState state) => new(state, EmissionKind.None);
|
||||
public static TransitionResult NoOp(AlarmConditionState state, string reason) => new(state, EmissionKind.None);
|
||||
public static TransitionResult NoOp(AlarmConditionState state, string reason)
|
||||
=> new(state, EmissionKind.None, reason);
|
||||
}
|
||||
|
||||
/// <summary>What kind of event, if any, the engine should emit after a transition.</summary>
|
||||
|
||||
Reference in New Issue
Block a user