64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public members surfaced by commentchecker — resolves 5,847 of 5,869 issues (99.6%) across three /fixdocs passes.
122 lines
5.3 KiB
C#
122 lines
5.3 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
|
|
|
|
/// <summary>
|
|
/// Production-side <see cref="IAlarmActorStateStore"/> backed by the
|
|
/// <see cref="ScriptedAlarmState"/> table in the central config DB. The actor's
|
|
/// 3-state enum projects into the table's two persisted dimensions: Acked + an
|
|
/// internal "_lastActiveState" recorded via a synthetic mapping (Inactive ⇒ Acked,
|
|
/// Active ⇒ Unacked, Acknowledged ⇒ Acked). ActiveState itself is deliberately NOT
|
|
/// persisted — re-derives from the evaluator on startup (Phase 7 decision #14).
|
|
/// </summary>
|
|
public sealed class EfAlarmActorStateStore : IAlarmActorStateStore
|
|
{
|
|
private readonly IDbContextFactory<OtOpcUaConfigDbContext> _dbFactory;
|
|
private readonly ILogger<EfAlarmActorStateStore> _logger;
|
|
|
|
/// <summary>Initializes a new instance of the EfAlarmActorStateStore.</summary>
|
|
/// <param name="dbFactory">The factory for creating database contexts.</param>
|
|
/// <param name="logger">The logger instance.</param>
|
|
public EfAlarmActorStateStore(
|
|
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
|
ILogger<EfAlarmActorStateStore> logger)
|
|
{
|
|
_dbFactory = dbFactory;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>Loads the alarm state snapshot from the database.</summary>
|
|
/// <param name="alarmId">The identifier of the alarm.</param>
|
|
/// <param name="ct">The cancellation token.</param>
|
|
/// <returns>The alarm state snapshot, or null if not found.</returns>
|
|
public async Task<AlarmActorStateSnapshot?> LoadAsync(string alarmId, CancellationToken ct)
|
|
{
|
|
using var db = await _dbFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
|
|
var row = await db.ScriptedAlarmStates.AsNoTracking()
|
|
.FirstOrDefaultAsync(r => r.ScriptedAlarmId == alarmId, ct)
|
|
.ConfigureAwait(false);
|
|
if (row is null) return null;
|
|
|
|
var state = MapAckedToActorState(row.AckedState);
|
|
return new AlarmActorStateSnapshot(
|
|
AlarmId: alarmId,
|
|
State: state,
|
|
LastTransitionUtc: row.UpdatedAtUtc,
|
|
LastAckUser: row.LastAckUser);
|
|
}
|
|
|
|
/// <summary>Saves the alarm state snapshot to the database.</summary>
|
|
/// <param name="snapshot">The alarm state snapshot to save.</param>
|
|
/// <param name="ct">The cancellation token.</param>
|
|
public async Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct)
|
|
{
|
|
using var db = await _dbFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
|
|
var row = await db.ScriptedAlarmStates
|
|
.FirstOrDefaultAsync(r => r.ScriptedAlarmId == snapshot.AlarmId, ct)
|
|
.ConfigureAwait(false);
|
|
|
|
var ackedState = MapActorStateToAcked(snapshot.State);
|
|
if (row is null)
|
|
{
|
|
db.ScriptedAlarmStates.Add(new ScriptedAlarmState
|
|
{
|
|
ScriptedAlarmId = snapshot.AlarmId,
|
|
EnabledState = "Enabled",
|
|
AckedState = ackedState,
|
|
ConfirmedState = "Confirmed",
|
|
ShelvingState = "Unshelved",
|
|
LastAckUser = snapshot.LastAckUser,
|
|
LastAckUtc = string.Equals(snapshot.State, "Acknowledged", StringComparison.Ordinal)
|
|
? snapshot.LastTransitionUtc
|
|
: null,
|
|
UpdatedAtUtc = snapshot.LastTransitionUtc,
|
|
CommentsJson = "[]",
|
|
});
|
|
}
|
|
else
|
|
{
|
|
row.AckedState = ackedState;
|
|
row.LastAckUser = snapshot.LastAckUser ?? row.LastAckUser;
|
|
if (string.Equals(snapshot.State, "Acknowledged", StringComparison.Ordinal))
|
|
row.LastAckUtc = snapshot.LastTransitionUtc;
|
|
row.UpdatedAtUtc = snapshot.LastTransitionUtc;
|
|
}
|
|
|
|
try
|
|
{
|
|
await db.SaveChangesAsync(ct).ConfigureAwait(false);
|
|
}
|
|
catch (DbUpdateConcurrencyException ex)
|
|
{
|
|
// Two actors racing to save the same alarm is benign — the last writer wins on
|
|
// UpdatedAtUtc, and the next transition on either side will write again. Log
|
|
// + drop so a race doesn't crash the dispatcher.
|
|
_logger.LogDebug(ex, "EfAlarmActorStateStore: concurrency conflict for {AlarmId}; dropping save",
|
|
snapshot.AlarmId);
|
|
}
|
|
}
|
|
|
|
private static string MapActorStateToAcked(string actorState) => actorState switch
|
|
{
|
|
"Active" => "Unacknowledged",
|
|
"Acknowledged" => "Acknowledged",
|
|
// Inactive maps to Acknowledged — when an alarm clears, nothing is left to ack.
|
|
_ => "Acknowledged",
|
|
};
|
|
|
|
private static string MapAckedToActorState(string ackedState)
|
|
{
|
|
// Only Active distinguishes from Acked — Inactive comes from a re-eval, not from
|
|
// the table. Persisted "Unacknowledged" implies the actor was last Active +
|
|
// un-acked; we restore it to Active so a restart doesn't drop pending operator work.
|
|
return string.Equals(ackedState, "Unacknowledged", StringComparison.Ordinal)
|
|
? "Active"
|
|
: "Acknowledged";
|
|
}
|
|
}
|