fix(scripted-alarms): atomic alarm-condition lookup under Lock (T15 review)

This commit is contained in:
Joseph Doherty
2026-06-10 19:45:24 -04:00
parent 4eb1d65e2b
commit ab5d0752d8
@@ -107,9 +107,11 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId); ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
ArgumentNullException.ThrowIfNull(state); ArgumentNullException.ThrowIfNull(state);
if (_alarmConditions.TryGetValue(alarmNodeId, out var condition)) // Look up + project under a SINGLE Lock so a concurrent RebuildAddressSpace can't clear
// _alarmConditions / detach the condition node between the lookup and the Set* calls.
lock (Lock)
{ {
lock (Lock) if (_alarmConditions.TryGetValue(alarmNodeId, out var condition))
{ {
// EnabledState / AckedState / ActiveState are mandatory children — always present after // EnabledState / AckedState / ActiveState are mandatory children — always present after
// Create. Confirm + Shelving are optional Part 9 children: T14's real-server finding is // Create. Confirm + Shelving are optional Part 9 children: T14's real-server finding is
@@ -147,15 +149,12 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
// NO ReportEvent here — T16 owns event firing. ClearChangeMasks just notifies any // NO ReportEvent here — T16 owns event firing. ClearChangeMasks just notifies any
// attribute (not event) subscribers watching the condition's children directly. // attribute (not event) subscribers watching the condition's children directly.
condition.ClearChangeMasks(SystemContext, includeChildren: true); condition.ClearChangeMasks(SystemContext, includeChildren: true);
return;
} }
return;
}
// Fallback: alarm not materialised as a real condition — keep the legacy bool[2] variable so // Fallback: alarm not materialised as a real condition — keep the legacy bool[2] variable so
// un-materialised callers (and the existing unit tests) keep working. // un-materialised callers (and the existing unit tests) keep working. CreateVariable mutates
lock (Lock) // the SDK address space, so it MUST run under Lock (see WriteValue).
{
// CreateVariable mutates the SDK address space, so it MUST run under Lock (see WriteValue).
if (!_variables.TryGetValue(alarmNodeId, out var variable)) if (!_variables.TryGetValue(alarmNodeId, out var variable))
{ {
variable = CreateVariable(alarmNodeId); variable = CreateVariable(alarmNodeId);