89204ac0fa
Review I-2. Under factory Props a supervision restart of ScriptedAlarmHostActor builds an empty engine, and nothing re-sent ApplyScriptedAlarms. Until the next deploy the host had no mux interest (inputs stopped), and its ownership filter silently dropped every operator ack and shelve. The host now tells its parent ScriptedAlarmHostRestarted from PostRestart. DriverHostActor keeps the last plans it applied and re-sends ApplyScriptedAlarms when the signal comes from the current host, in Steady, Applying and Stale. This mirrors how a driver child's ReprovisionRequested is answered. The OnAlarmsLoaded comment, which assumed a re-apply that never happened, now says where it comes from. DriverHostActorScriptedAlarmRestartTests deploys one scripted alarm through a real host, crashes the alarm host, does NOT re-apply by hand, and asserts that interest is re-registered, a dependency push activates the alarm, and an operator ack reaches the engine. It is red without the signal (times out waiting for the re-registration). Refs #731 #734 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
104 lines
5.2 KiB
C#
104 lines
5.2 KiB
C#
using System.Text.Json;
|
|
using Akka.Actor;
|
|
using Serilog;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
|
|
|
/// <summary>
|
|
/// Review I-2: under the factory Props a supervision restart of the scripted-alarm host builds a fresh, EMPTY
|
|
/// engine. Nothing re-sent <c>ApplyScriptedAlarms</c>, so until the next deploy the host had no mux interest
|
|
/// (inputs stopped) and its ownership filter dropped every operator ack/shelve as "not owned". The host now
|
|
/// tells its parent <see cref="ScriptedAlarms.ScriptedAlarmHostActor.ScriptedAlarmHostRestarted"/> from <c>PostRestart</c>,
|
|
/// and <see cref="DriverHostActor"/> re-sends the last plans it applied. This test never re-applies by hand.
|
|
/// </summary>
|
|
public sealed class DriverHostActorScriptedAlarmRestartTests : RuntimeActorTestBase
|
|
{
|
|
private static readonly NodeId TestNode = NodeId.Parse("driver-alarmhost-restart-test");
|
|
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('d', 64));
|
|
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
|
|
|
|
private static DeploymentId SeedAlarmDeployment(Microsoft.EntityFrameworkCore.IDbContextFactory<ZB.MOM.WW.OtOpcUa.Configuration.OtOpcUaConfigDbContext> db)
|
|
{
|
|
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
ScriptedAlarms = new[]
|
|
{
|
|
new
|
|
{
|
|
ScriptedAlarmId = "alm-1", EquipmentId = "eq-1", Name = "HighTemp", AlarmType = "AlarmCondition",
|
|
Severity = 800, MessageTemplate = "hot", PredicateScriptId = "s-1",
|
|
HistorizeToAveva = false, Retain = true, Enabled = true,
|
|
},
|
|
},
|
|
Scripts = new[] { new { ScriptId = "s-1", SourceCode = "return (int)ctx.GetTag(\"M.T\").Value > 90;" } },
|
|
});
|
|
var id = DeploymentId.NewId();
|
|
using var ctx = db.CreateDbContext();
|
|
ctx.Deployments.Add(new Deployment
|
|
{
|
|
DeploymentId = id.Value, RevisionHash = RevA.Value, Status = DeploymentStatus.Sealed,
|
|
CreatedBy = "test", SealedAtUtc = DateTime.UtcNow, ArtifactBlob = artifact,
|
|
});
|
|
ctx.SaveChanges();
|
|
return id;
|
|
}
|
|
|
|
[Fact]
|
|
public void After_a_supervision_restart_the_alarm_host_is_re_applied_and_inputs_and_acks_work_again()
|
|
{
|
|
var db = NewInMemoryDbFactory();
|
|
var deploymentId = SeedAlarmDeployment(db);
|
|
var publish = this.CreateCleanOutcomePublishProbe();
|
|
var mux = CreateTestProbe();
|
|
var coordinator = CreateTestProbe();
|
|
var host = Sys.ActorOf(DriverHostActor.Props(
|
|
db, TestNode, coordinator.Ref,
|
|
localRoles: new HashSet<string> { "driver" },
|
|
dependencyMux: mux.Ref,
|
|
opcUaPublishActor: publish.Ref,
|
|
virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
|
|
virtualTagHostOverride: CreateTestProbe().Ref,
|
|
scriptRootLogger: new ScriptRootLogger(new LoggerConfiguration().CreateLogger()),
|
|
alarmStateStore: new InMemoryAlarmStateStore()));
|
|
V3ArtifactSeeder.Dispatch(host, coordinator, deploymentId, RevA);
|
|
|
|
var first = mux.FishForMessage<DependencyMuxActor.RegisterInterest>(r => r.TagRefs.Contains("M.T"), Timeout);
|
|
var alarmHost = first.Subscriber;
|
|
|
|
// The crash: a batch item with a null TagId makes the upstream Push throw (the documented unguarded
|
|
// path), so supervision restarts the alarm host. NO manual re-apply follows.
|
|
alarmHost.Tell(new VirtualTagActor.DependencyValueBatch(new[]
|
|
{
|
|
new VirtualTagActor.DependencyValueChanged(null!, 1, DateTime.UtcNow),
|
|
}));
|
|
mux.FishForMessage<DependencyMuxActor.UnregisterInterest>(u => u.Subscriber.Equals(alarmHost), Timeout);
|
|
|
|
// The parent re-applies on the restart signal: interest is registered again by the new incarnation.
|
|
mux.FishForMessage<DependencyMuxActor.RegisterInterest>(r => r.TagRefs.Contains("M.T"), Timeout);
|
|
|
|
// Inputs work: a dependency push activates the alarm.
|
|
alarmHost.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
|
|
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(
|
|
m => m.AlarmNodeId == "alm-1" && m.State.Active && !m.State.Acknowledged, Timeout);
|
|
|
|
// Acks work: the restarted engine owns alm-1 again, so the command is not filtered out as unowned.
|
|
alarmHost.Tell(new AlarmCommand(AlarmId: "alm-1", Operation: "Acknowledge", User: "alice", Comment: null, UnshelveAtUtc: null));
|
|
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(
|
|
m => m.AlarmNodeId == "alm-1" && m.State.Acknowledged, Timeout)
|
|
.State.Acknowledged.ShouldBeTrue();
|
|
}
|
|
}
|