From a0b3a4c8a712618c23a293e4831820fd305ecc10 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 22 May 2026 08:18:42 -0400 Subject: [PATCH] fix(scripted-alarms): resolve Medium code-review finding (Core.ScriptedAlarms-007) Reorder persist/update in ApplyAsync, ReevaluateAsync, and ShelvingCheckAsync: SaveAsync is now called before the in-memory _alarms entry is advanced. A store failure therefore leaves both the persisted and in-memory views at the prior state rather than diverging, maintaining the invariant that startup recovery reflects actual persisted state. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ScriptedAlarmEngine.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs index e23d80a..cc3f4e9 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs @@ -238,8 +238,12 @@ public sealed class ScriptedAlarmEngine : IDisposable try { var result = op(state.Condition); - _alarms[alarmId] = state with { Condition = result.State }; + // Persist BEFORE updating in-memory so a store failure leaves both + // in-memory and persisted at the prior state rather than diverging. + // If SaveAsync throws the in-memory _alarms entry stays unchanged and + // the exception propagates to the caller. (Core.ScriptedAlarms-007) await _store.SaveAsync(result.State, ct).ConfigureAwait(false); + _alarms[alarmId] = state with { Condition = result.State }; if (result.Emission != EmissionKind.None) EmitEvent(state, result.State, result.Emission); } finally { _evalGate.Release(); } @@ -279,8 +283,10 @@ public sealed class ScriptedAlarmEngine : IDisposable state, state.Condition, _clock(), ct).ConfigureAwait(false); if (!ReferenceEquals(newState, state.Condition)) { - _alarms[id] = state with { Condition = newState }; + // Persist before updating in-memory so a store failure leaves + // both sides at the prior state. (Core.ScriptedAlarms-007) await _store.SaveAsync(newState, ct).ConfigureAwait(false); + _alarms[id] = state with { Condition = newState }; } } } @@ -422,8 +428,10 @@ public sealed class ScriptedAlarmEngine : IDisposable var result = Part9StateMachine.ApplyShelvingCheck(state.Condition, now); if (!ReferenceEquals(result.State, state.Condition)) { - _alarms[id] = state with { Condition = result.State }; + // Persist before updating in-memory so a store failure leaves + // both sides at the prior state. (Core.ScriptedAlarms-007) await _store.SaveAsync(result.State, ct).ConfigureAwait(false); + _alarms[id] = state with { Condition = result.State }; if (result.Emission != EmissionKind.None) EmitEvent(state, result.State, result.Emission); }