fix(runtime): prune the native-alarm projector on deploy instead of clearing it (#641)
DriverHostActor cleared NativeAlarmProjector's per-condition prior state on EVERY apply, so after any deploy a driver Acknowledge of a still-active alarm projected Active=false (writing it inactive) and a Clear of an unacknowledged alarm projected Acked=true (silently acknowledging it). The projector is now pruned to the conditions the new composition still carries (RetainOnly). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+68
-1
@@ -409,6 +409,66 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #641 — a redeploy that keeps the alarm must not reset the projector's prior state for it. It used
|
||||
/// to be cleared on EVERY apply, so a driver Acknowledge arriving after any deploy projected
|
||||
/// <c>(prev.Active = false, Acked)</c> and wrote an ACTIVE alarm inactive.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void An_acknowledge_after_a_redeploy_keeps_the_alarm_active()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedV3AlarmDeployment(db, RevA, Driver: "drv-1", Tag: "temp_hi");
|
||||
var (actor, publish, coordinator) = SpawnHostAndApplyWithCoordinator(db, deploymentId);
|
||||
|
||||
actor.Tell(Transition(AlarmTransitionKind.Raise));
|
||||
publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(TimeSpan.FromSeconds(5)).State.Active.ShouldBeTrue();
|
||||
|
||||
Redeploy(db, actor, publish, coordinator);
|
||||
|
||||
actor.Tell(Transition(AlarmTransitionKind.Acknowledge));
|
||||
var acked = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(_ => true, TimeSpan.FromSeconds(5));
|
||||
acked.State.Active.ShouldBeTrue("the alarm is still active at the device");
|
||||
acked.State.Acknowledged.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Gitea #641 — likewise a Clear after a redeploy must not silently acknowledge an unacknowledged
|
||||
/// alarm (the cleared prior state defaulted Acked to true).</summary>
|
||||
[Fact]
|
||||
public void A_clear_after_a_redeploy_keeps_the_alarm_unacknowledged()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedV3AlarmDeployment(db, RevA, Driver: "drv-1", Tag: "temp_hi");
|
||||
var (actor, publish, coordinator) = SpawnHostAndApplyWithCoordinator(db, deploymentId);
|
||||
|
||||
actor.Tell(Transition(AlarmTransitionKind.Raise));
|
||||
publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(TimeSpan.FromSeconds(5)).State.Acknowledged.ShouldBeFalse();
|
||||
|
||||
Redeploy(db, actor, publish, coordinator);
|
||||
|
||||
actor.Tell(Transition(AlarmTransitionKind.Clear));
|
||||
var cleared = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(_ => true, TimeSpan.FromSeconds(5));
|
||||
cleared.State.Active.ShouldBeFalse();
|
||||
cleared.State.Acknowledged.ShouldBeFalse("nobody acknowledged it");
|
||||
}
|
||||
|
||||
private static DriverInstanceActor.AttributeAlarmPublished Transition(AlarmTransitionKind kind) =>
|
||||
new("drv-1", new AlarmEventArgs(
|
||||
new StubAlarmHandle(), "Temp", AlarmRawPath, "OffNormalAlarm", kind.ToString(), AlarmSeverity.High, Ts,
|
||||
Kind: kind));
|
||||
|
||||
/// <summary>A second deployment carrying the SAME alarm (a new revision of an identical artifact).</summary>
|
||||
private void Redeploy(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> db, IActorRef actor, TestProbe publish, TestProbe coordinator)
|
||||
{
|
||||
var revB = RevisionHash.Parse(new string('b', 64));
|
||||
var second = SeedV3AlarmDeployment(db, revB, Driver: "drv-1", Tag: "temp_hi");
|
||||
actor.Tell(new DispatchDeployment(second, revB, CorrelationId.NewId()));
|
||||
coordinator.FishForMessage<ApplyAck>(a => a.DeploymentId == second, TimeSpan.FromSeconds(5))
|
||||
.Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
publish.FishForMessage<OpcUaPublishActor.RebuildAddressSpace>(_ => true, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>Subscribe <paramref name="probe"/> to the <c>alerts</c> DPS topic and wait for the ack.</summary>
|
||||
private void SubscribeToAlerts(TestProbe probe)
|
||||
{
|
||||
@@ -439,6 +499,13 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase
|
||||
/// publishes an alarm. A VirtualTag-host probe is injected so the real host isn't spawned.</summary>
|
||||
private (IActorRef Actor, TestProbe Publish) SpawnHostAndApply(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> db, DeploymentId deploymentId)
|
||||
{
|
||||
var (actor, publish, _) = SpawnHostAndApplyWithCoordinator(db, deploymentId);
|
||||
return (actor, publish);
|
||||
}
|
||||
|
||||
private (IActorRef Actor, TestProbe Publish, TestProbe Coordinator) SpawnHostAndApplyWithCoordinator(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> db, DeploymentId deploymentId)
|
||||
{
|
||||
var coordinator = CreateTestProbe();
|
||||
var publish = CreateTestProbe();
|
||||
@@ -460,7 +527,7 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase
|
||||
// ExpectMsg<AlarmStateUpdate> assertions see only alarm updates.
|
||||
publish.ExpectMsg<OpcUaPublishActor.RebuildAddressSpace>(TimeSpan.FromSeconds(5));
|
||||
|
||||
return (actor, publish);
|
||||
return (actor, publish, coordinator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -24,6 +24,21 @@ public class NativeAlarmProjectorTests
|
||||
snap.Message.ShouldBe("boom");
|
||||
}
|
||||
|
||||
/// <summary>Gitea #641 — <c>RetainOnly</c> keeps a still-deployed condition's prior state and forgets a removed
|
||||
/// one's (which then starts from the default inactive + acked).</summary>
|
||||
[Fact]
|
||||
public void RetainOnly_keeps_deployed_conditions_and_forgets_removed_ones()
|
||||
{
|
||||
var sut = new NativeAlarmProjector();
|
||||
sut.Project("kept", Evt(AlarmTransitionKind.Raise));
|
||||
sut.Project("removed", Evt(AlarmTransitionKind.Raise));
|
||||
|
||||
sut.RetainOnly(new HashSet<string>(StringComparer.Ordinal) { "kept" });
|
||||
|
||||
sut.Project("kept", Evt(AlarmTransitionKind.Acknowledge)).Active.ShouldBeTrue();
|
||||
sut.Project("removed", Evt(AlarmTransitionKind.Acknowledge)).Active.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retrigger_projects_active_unacked()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user