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);
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
// 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
// attribute (not event) subscribers watching the condition's children directly.
condition.ClearChangeMasks(SystemContext, includeChildren: true);
return;
}
return;
}
// 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.
lock (Lock)
{
// CreateVariable mutates the SDK address space, so it MUST run under Lock (see WriteValue).
// 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. CreateVariable mutates
// the SDK address space, so it MUST run under Lock (see WriteValue).
if (!_variables.TryGetValue(alarmNodeId, out var variable))
{
variable = CreateVariable(alarmNodeId);