fix(scripted-alarms): ScriptedAlarmHostActor review fixes — load-gen guard, quiet cancel, parse guard (T9 review)
This commit is contained in:
+67
@@ -50,6 +50,23 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
|
||||
Retain: true,
|
||||
Enabled: enabled);
|
||||
|
||||
/// <summary>Plan whose predicate references an unknown identifier so it fails to compile — applying it
|
||||
/// faults the engine's LoadAsync. Used to prove a faulted load doesn't crash the host.</summary>
|
||||
private static EquipmentScriptedAlarmPlan BadPlan(string id = "bad-1") =>
|
||||
new(
|
||||
ScriptedAlarmId: id,
|
||||
EquipmentId: "Plant/Line1/M",
|
||||
Name: "Broken",
|
||||
AlarmType: "AlarmCondition",
|
||||
Severity: 500,
|
||||
MessageTemplate: "broken",
|
||||
PredicateScriptId: $"{id}-script",
|
||||
PredicateSource: "return unknownIdentifier;", // uncompilable → LoadAsync throws
|
||||
DependencyRefs: Array.Empty<string>(),
|
||||
HistorizeToAveva: false,
|
||||
Retain: false,
|
||||
Enabled: true);
|
||||
|
||||
private static ScriptedAlarmEngine BuildEngine(DependencyMuxTagUpstreamSource upstream)
|
||||
{
|
||||
var logger = new LoggerConfiguration().CreateLogger();
|
||||
@@ -170,4 +187,54 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
|
||||
second.TagRefs.ShouldContain("M.Y");
|
||||
second.TagRefs.ShouldNotContain("M.T");
|
||||
}
|
||||
|
||||
/// <summary>Faulted load resilience: applying an alarm whose predicate doesn't compile faults the
|
||||
/// engine's LoadAsync. The host logs a Warning (Status.Failure handler) and stays alive — it must
|
||||
/// still process a subsequent valid apply, registering interest for that apply's dep refs.</summary>
|
||||
[Fact]
|
||||
public void Faulted_load_does_not_crash_host()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var (host, _) = Spawn(publish, mux);
|
||||
|
||||
// Apply a plan that fails to compile — LoadAsync faults, the host swallows it as a Warning and
|
||||
// does NOT register interest (no dep refs to register, and the load never completed).
|
||||
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { BadPlan() }));
|
||||
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
|
||||
// Prove the actor is still responsive: a later valid apply loads + registers interest as normal.
|
||||
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-ok", depRef: "M.T") }));
|
||||
var reg = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout);
|
||||
reg.TagRefs.ShouldContain("M.T");
|
||||
}
|
||||
|
||||
/// <summary>Stale-load guard (fix A): two back-to-back applies with different dep-ref sets must end
|
||||
/// with the mux holding the SECOND (latest-generation) set — a stale earlier completion must never
|
||||
/// re-introduce the first set's refs.
|
||||
///
|
||||
/// <para>Limitation: forcing the two LoadAsync continuations to complete out of order is not
|
||||
/// deterministic via real async timing (both loads are short + run on the thread pool). This test
|
||||
/// therefore validates the guard's observable contract rather than the race itself: it fishes for
|
||||
/// the RegisterInterest carrying the second apply's refs, then asserts no LATER RegisterInterest
|
||||
/// re-introduces the first apply's refs. With the generation guard in place the latest apply always
|
||||
/// wins; without it, an out-of-order stale completion could land a "M.A"-bearing RegisterInterest
|
||||
/// after the "M.B" one — which this assertion would catch.</para></summary>
|
||||
[Fact]
|
||||
public void Stale_load_does_not_register_superseded_dep_refs()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var (host, _) = Spawn(publish, mux);
|
||||
|
||||
// Fire two applies in quick succession with disjoint dep refs; the second supersedes the first.
|
||||
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-a", depRef: "M.A") }));
|
||||
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-b", depRef: "M.B") }));
|
||||
|
||||
// The latest generation must win: fish for the RegisterInterest reflecting the second apply.
|
||||
mux.FishForMessage<DependencyMuxActor.RegisterInterest>(r => r.TagRefs.Contains("M.B"), Timeout);
|
||||
|
||||
// No LATER RegisterInterest may re-introduce the first (superseded) apply's "M.A" ref.
|
||||
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user