feat(siteruntime): InstanceActor spawns NativeAlarmActors + enriched alarm snapshot; clear native state on redeploy/undeploy
This commit is contained in:
@@ -422,8 +422,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
command.RevisionHash,
|
||||
isEnabled: true);
|
||||
|
||||
// Static overrides are reset on redeployment per design decision
|
||||
// Static overrides and mirrored native alarm state are reset on
|
||||
// redeployment per design decision — the new config may bind different
|
||||
// sources, and the source snapshot re-seeds the mirror on (re)subscribe.
|
||||
await _storage.ClearStaticOverridesAsync(instanceName);
|
||||
await _storage.ClearNativeAlarmsForInstanceAsync(instanceName);
|
||||
|
||||
// Replicate to standby node
|
||||
_replicationActor?.Tell(new ReplicateConfigDeploy(
|
||||
|
||||
@@ -48,6 +48,13 @@ public class InstanceActor : ReceiveActor
|
||||
private readonly Dictionary<string, int> _alarmPriorities = new();
|
||||
private readonly Dictionary<string, IActorRef> _scriptActors = new();
|
||||
private readonly Dictionary<string, IActorRef> _alarmActors = new();
|
||||
private readonly Dictionary<string, IActorRef> _nativeAlarmActors = new();
|
||||
/// <summary>
|
||||
/// Latest enriched <see cref="AlarmStateChanged"/> per alarm name (computed and
|
||||
/// native), so the DebugView snapshot carries the unified condition + native
|
||||
/// metadata rather than a bare State/Priority projection.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, AlarmStateChanged> _latestAlarmEvents = new();
|
||||
private FlattenedConfiguration? _configuration;
|
||||
|
||||
// DCL manager actor reference for subscribing to tag values
|
||||
@@ -505,6 +512,9 @@ public class InstanceActor : ReceiveActor
|
||||
{
|
||||
_alarmStates[changed.AlarmName] = changed.State;
|
||||
_alarmTimestamps[changed.AlarmName] = changed.Timestamp;
|
||||
// Retain the full enriched event (Kind, Condition, native metadata) so the
|
||||
// DebugView snapshot reflects it — native alarms have no _alarmActors entry.
|
||||
_latestAlarmEvents[changed.AlarmName] = changed;
|
||||
|
||||
// WP-23: Publish to site-wide stream
|
||||
_streamManager?.PublishAlarmStateChanged(changed);
|
||||
@@ -525,17 +535,10 @@ public class InstanceActor : ReceiveActor
|
||||
_attributeQualities.GetValueOrDefault(kvp.Key, "Good"),
|
||||
_attributeTimestamps.GetValueOrDefault(kvp.Key, now))).ToList();
|
||||
|
||||
var alarmStates = _alarmActors.Keys.Select(name => new AlarmStateChanged(
|
||||
_instanceUniqueName,
|
||||
name,
|
||||
_alarmStates.GetValueOrDefault(name, AlarmState.Normal),
|
||||
_alarmPriorities.GetValueOrDefault(name, 0),
|
||||
_alarmTimestamps[name])).ToList();
|
||||
|
||||
var snapshot = new DebugViewSnapshot(
|
||||
_instanceUniqueName,
|
||||
attributeValues,
|
||||
alarmStates,
|
||||
BuildAlarmStatesSnapshot(),
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
Sender.Tell(snapshot);
|
||||
@@ -545,6 +548,34 @@ public class InstanceActor : ReceiveActor
|
||||
_instanceUniqueName, request.CorrelationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the alarm-state list for a DebugView snapshot. Prefers the latest
|
||||
/// enriched <see cref="AlarmStateChanged"/> per alarm (computed alarms that have
|
||||
/// fired plus all native alarms) and falls back to a bare Normal projection for
|
||||
/// computed alarms that have not yet emitted an event.
|
||||
/// </summary>
|
||||
private List<AlarmStateChanged> BuildAlarmStatesSnapshot()
|
||||
{
|
||||
var states = _latestAlarmEvents.Values.ToList();
|
||||
|
||||
foreach (var name in _alarmActors.Keys)
|
||||
{
|
||||
if (_latestAlarmEvents.ContainsKey(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
states.Add(new AlarmStateChanged(
|
||||
_instanceUniqueName,
|
||||
name,
|
||||
_alarmStates.GetValueOrDefault(name, AlarmState.Normal),
|
||||
_alarmPriorities.GetValueOrDefault(name, 0),
|
||||
_alarmTimestamps.GetValueOrDefault(name, DateTimeOffset.UtcNow)));
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-25: Debug view unsubscribe (SiteRuntime-013).
|
||||
/// This handler is a deliberate no-op acknowledgement: the Instance Actor holds
|
||||
@@ -576,17 +607,10 @@ public class InstanceActor : ReceiveActor
|
||||
_attributeQualities.GetValueOrDefault(kvp.Key, "Good"),
|
||||
_attributeTimestamps.GetValueOrDefault(kvp.Key, now))).ToList();
|
||||
|
||||
var alarmStates = _alarmActors.Keys.Select(name => new AlarmStateChanged(
|
||||
_instanceUniqueName,
|
||||
name,
|
||||
_alarmStates.GetValueOrDefault(name, AlarmState.Normal),
|
||||
_alarmPriorities.GetValueOrDefault(name, 0),
|
||||
_alarmTimestamps[name])).ToList();
|
||||
|
||||
var snapshot = new DebugViewSnapshot(
|
||||
_instanceUniqueName,
|
||||
attributeValues,
|
||||
alarmStates,
|
||||
BuildAlarmStatesSnapshot(),
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
Sender.Tell(snapshot);
|
||||
@@ -747,9 +771,55 @@ public class InstanceActor : ReceiveActor
|
||||
_alarmTimestamps[alarm.CanonicalName] = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
// Create Native Alarm Actors — read-only mirror of each bound source's
|
||||
// native alarms (peers to the computed Alarm Actors). They subscribe through
|
||||
// the DCL manager, so without one (e.g. in isolated tests) they are skipped.
|
||||
foreach (var nativeSource in _configuration.NativeAlarmSources)
|
||||
{
|
||||
if (_dclManager == null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Instance {Instance}: native alarm source {Source} skipped — no DCL manager available",
|
||||
_instanceUniqueName, nativeSource.CanonicalName);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nativeKind = ResolveNativeKind(nativeSource.ConnectionName);
|
||||
var props = Props.Create(() => new NativeAlarmActor(
|
||||
nativeSource,
|
||||
_instanceUniqueName,
|
||||
Self,
|
||||
_dclManager,
|
||||
_storage,
|
||||
_options,
|
||||
_logger,
|
||||
nativeKind));
|
||||
|
||||
var actorRef = Context.ActorOf(props, $"native-alarm-{nativeSource.CanonicalName}");
|
||||
_nativeAlarmActors[nativeSource.CanonicalName] = actorRef;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Instance {Instance}: created {Scripts} script actors and {Alarms} alarm actors",
|
||||
_instanceUniqueName, _scriptActors.Count, _alarmActors.Count);
|
||||
"Instance {Instance}: created {Scripts} script actors, {Alarms} alarm actors, {NativeAlarms} native alarm actors",
|
||||
_instanceUniqueName, _scriptActors.Count, _alarmActors.Count, _nativeAlarmActors.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a bound connection's protocol to the native alarm kind stamped on
|
||||
/// emitted events. MxAccess Gateway connections yield
|
||||
/// <see cref="AlarmKind.NativeMxAccess"/>; everything else (OPC UA) yields
|
||||
/// <see cref="AlarmKind.NativeOpcUa"/>.
|
||||
/// </summary>
|
||||
private AlarmKind ResolveNativeKind(string connectionName)
|
||||
{
|
||||
var protocol = _configuration?.Connections is { } connections
|
||||
&& connections.TryGetValue(connectionName, out var cfg)
|
||||
? cfg.Protocol
|
||||
: null;
|
||||
|
||||
return protocol != null && protocol.Contains("Mx", StringComparison.OrdinalIgnoreCase)
|
||||
? AlarmKind.NativeMxAccess
|
||||
: AlarmKind.NativeOpcUa;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -244,6 +244,14 @@ public class SiteStorageService
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
await using (var cmd = connection.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = (SqliteTransaction)transaction;
|
||||
cmd.CommandText = "DELETE FROM native_alarm_state WHERE instance_unique_name = @name";
|
||||
cmd.Parameters.AddWithValue("@name", instanceName);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
await using (var cmd = connection.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = (SqliteTransaction)transaction;
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
using System.Text.Json;
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// Task 16: InstanceActor spawns NativeAlarmActor children from the flattened
|
||||
/// configuration and surfaces enriched native alarm state in the DebugView snapshot.
|
||||
/// </summary>
|
||||
public class InstanceActorNativeAlarmTests : TestKit, IDisposable
|
||||
{
|
||||
private readonly SiteStorageService _storage;
|
||||
private readonly ScriptCompilationService _compilationService;
|
||||
private readonly SharedScriptLibrary _sharedScriptLibrary;
|
||||
private readonly SiteRuntimeOptions _options = new();
|
||||
private readonly string _dbFile;
|
||||
|
||||
public InstanceActorNativeAlarmTests()
|
||||
{
|
||||
_dbFile = Path.Combine(Path.GetTempPath(), $"instance-native-{Guid.NewGuid():N}.db");
|
||||
_storage = new SiteStorageService($"Data Source={_dbFile}", NullLogger<SiteStorageService>.Instance);
|
||||
_storage.InitializeAsync().GetAwaiter().GetResult();
|
||||
_compilationService = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
|
||||
_sharedScriptLibrary = new SharedScriptLibrary(_compilationService, NullLogger<SharedScriptLibrary>.Instance);
|
||||
}
|
||||
|
||||
private IActorRef CreateInstanceActorWithDcl(string instanceName, FlattenedConfiguration config, IActorRef dclManager) =>
|
||||
ActorOf(Props.Create(() => new InstanceActor(
|
||||
instanceName,
|
||||
JsonSerializer.Serialize(config),
|
||||
_storage,
|
||||
_compilationService,
|
||||
_sharedScriptLibrary,
|
||||
null,
|
||||
_options,
|
||||
NullLogger<InstanceActor>.Instance,
|
||||
dclManager)));
|
||||
|
||||
private static FlattenedConfiguration ConfigWithNativeSource(string instanceName) => new()
|
||||
{
|
||||
InstanceUniqueName = instanceName,
|
||||
NativeAlarmSources =
|
||||
[
|
||||
new ResolvedNativeAlarmSource
|
||||
{
|
||||
CanonicalName = "Pressure",
|
||||
ConnectionName = "Opc",
|
||||
SourceReference = "ns=2;s=T01"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void SpawnsNativeAlarmActor_WhichSubscribesViaDcl()
|
||||
{
|
||||
var dcl = CreateTestProbe();
|
||||
CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
|
||||
|
||||
var req = dcl.ExpectMsg<SubscribeAlarmsRequest>();
|
||||
Assert.Equal("inst", req.InstanceUniqueName);
|
||||
Assert.Equal("Opc", req.ConnectionName);
|
||||
Assert.Equal("ns=2;s=T01", req.SourceReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DebugViewSnapshot_IncludesNativeAlarm_AfterTransition()
|
||||
{
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
|
||||
|
||||
// Simulate the NativeAlarmActor emitting an enriched event upward.
|
||||
actor.Tell(new AlarmStateChanged("inst", "T01.Hi", AlarmState.Active, 800, DateTimeOffset.UtcNow)
|
||||
{
|
||||
Kind = AlarmKind.NativeOpcUa,
|
||||
SourceReference = "T01.Hi",
|
||||
Condition = new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800)
|
||||
});
|
||||
|
||||
actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
|
||||
var snap = ExpectMsg<DebugViewSnapshot>();
|
||||
|
||||
Assert.Contains(snap.AlarmStates, a =>
|
||||
a.SourceReference == "T01.Hi" && a.Kind == AlarmKind.NativeOpcUa && a.Condition.Severity == 800);
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
Shutdown();
|
||||
try { File.Delete(_dbFile); } catch { /* cleanup */ }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user