fix(server): resolve Medium code-review finding (Server-005)

Add _nodeManagerDisposed field; set it under Lock in Dispose before
detaching the alarm-service handler; check it in OnAlarmServiceTransition
under the same Lock so an in-flight transition cannot dispatch to a
ConditionSink whose DriverNodeManager is being concurrently disposed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 10:56:01 -04:00
parent 2003b343bf
commit 8e8199752f
2 changed files with 20 additions and 5 deletions

View File

@@ -110,6 +110,8 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
private readonly AlarmConditionService? _alarmService;
private readonly Dictionary<string, ConditionSink> _conditionSinks = new(StringComparer.OrdinalIgnoreCase);
private EventHandler<AlarmConditionTransition>? _alarmTransitionHandler;
// Guards OnAlarmServiceTransition against dispatch to a disposed node manager (Server-005).
private bool _nodeManagerDisposed;
// Task #24 — Phase 7 Gap 1: route OPC UA Part 9 Acknowledge / Confirm method calls on
// scripted alarm condition nodes to the ScriptedAlarmEngine so the engine state machine
@@ -179,6 +181,11 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
ConditionSink? sink;
lock (Lock)
{
// Guard against a transition that arrives after Dispose has begun (Server-005).
// The alarm service is shared across drivers, so detaching the handler in Dispose
// does not synchronise against an in-flight Invoke — check _nodeManagerDisposed
// under the same lock Dispose sets it under so we never forward to a torn-down sink.
if (_nodeManagerDisposed) return;
_conditionSinks.TryGetValue(t.ConditionId, out sink);
}
if (sink is null) return;
@@ -302,10 +309,18 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
/// </summary>
protected override void Dispose(bool disposing)
{
if (disposing && _alarmService is not null && _alarmTransitionHandler is not null)
if (disposing)
{
_alarmService.TransitionRaised -= _alarmTransitionHandler;
_alarmTransitionHandler = null;
// Mark disposed under Lock before detaching the handler so an in-flight
// OnAlarmServiceTransition that already holds the lock sees _nodeManagerDisposed
// and exits without forwarding to the sink (Server-005).
lock (Lock) { _nodeManagerDisposed = true; }
if (_alarmService is not null && _alarmTransitionHandler is not null)
{
_alarmService.TransitionRaised -= _alarmTransitionHandler;
_alarmTransitionHandler = null;
}
}
base.Dispose(disposing);
}