fix(scripted-alarms): lock CreateVariable + RemoveRootNotifier on rebuild (T14 review)
This commit is contained in:
@@ -30,8 +30,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
|||||||
private readonly ConcurrentDictionary<string, FolderState> _folders = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, FolderState> _folders = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, AlarmConditionState> _alarmConditions = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, AlarmConditionState> _alarmConditions = new(StringComparer.Ordinal);
|
||||||
/// <summary>Folders we have already promoted to event-notifiers + registered as root notifiers,
|
/// <summary>Folders we have already promoted to event-notifiers + registered as root notifiers,
|
||||||
/// so repeated <see cref="MaterialiseAlarmCondition"/> calls don't double-add (idempotent guard).</summary>
|
/// so repeated <see cref="MaterialiseAlarmCondition"/> calls don't double-add (idempotent guard).
|
||||||
private readonly HashSet<NodeId> _notifierFolders = new();
|
/// Keyed by NodeId → the actual <see cref="FolderState"/> so <see cref="RebuildAddressSpace"/> can
|
||||||
|
/// pass the folder to <c>RemoveRootNotifier</c> on teardown.</summary>
|
||||||
|
private readonly Dictionary<NodeId, FolderState> _notifierFolders = new();
|
||||||
private FolderState? _root;
|
private FolderState? _root;
|
||||||
|
|
||||||
/// <summary>Initializes a new instance of the <see cref="OtOpcUaNodeManager"/> class with the OPC UA server and configuration.</summary>
|
/// <summary>Initializes a new instance of the <see cref="OtOpcUaNodeManager"/> class with the OPC UA server and configuration.</summary>
|
||||||
@@ -69,10 +71,17 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
|||||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrEmpty(nodeId);
|
ArgumentException.ThrowIfNullOrEmpty(nodeId);
|
||||||
var variable = _variables.GetOrAdd(nodeId, CreateVariable);
|
|
||||||
|
|
||||||
lock (Lock)
|
lock (Lock)
|
||||||
{
|
{
|
||||||
|
// CreateVariable mutates the SDK address space (_root.AddChild + AddPredefinedNode),
|
||||||
|
// so it MUST run under Lock — the SDK's subscription/ConditionRefresh threads take it too.
|
||||||
|
if (!_variables.TryGetValue(nodeId, out var variable))
|
||||||
|
{
|
||||||
|
variable = CreateVariable(nodeId);
|
||||||
|
_variables[nodeId] = variable;
|
||||||
|
}
|
||||||
|
|
||||||
variable.Value = value;
|
variable.Value = value;
|
||||||
variable.StatusCode = StatusFromQuality(quality);
|
variable.StatusCode = StatusFromQuality(quality);
|
||||||
variable.Timestamp = sourceTimestampUtc;
|
variable.Timestamp = sourceTimestampUtc;
|
||||||
@@ -117,9 +126,15 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: alarm not materialised as a real condition — keep the legacy bool[2] variable.
|
// Fallback: alarm not materialised as a real condition — keep the legacy bool[2] variable.
|
||||||
var variable = _variables.GetOrAdd(alarmNodeId, CreateVariable);
|
|
||||||
lock (Lock)
|
lock (Lock)
|
||||||
{
|
{
|
||||||
|
// CreateVariable mutates the SDK address space, so it MUST run under Lock (see WriteValue).
|
||||||
|
if (!_variables.TryGetValue(alarmNodeId, out var variable))
|
||||||
|
{
|
||||||
|
variable = CreateVariable(alarmNodeId);
|
||||||
|
_variables[alarmNodeId] = variable;
|
||||||
|
}
|
||||||
|
|
||||||
variable.Value = new[] { active, acknowledged };
|
variable.Value = new[] { active, acknowledged };
|
||||||
variable.StatusCode = StatusCodes.Good;
|
variable.StatusCode = StatusCodes.Good;
|
||||||
variable.Timestamp = sourceTimestampUtc;
|
variable.Timestamp = sourceTimestampUtc;
|
||||||
@@ -230,7 +245,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
|||||||
/// alarm condition has a notifier path to the Server object for T16's event propagation.</summary>
|
/// alarm condition has a notifier path to the Server object for T16's event propagation.</summary>
|
||||||
private void EnsureFolderIsEventNotifier(FolderState folder)
|
private void EnsureFolderIsEventNotifier(FolderState folder)
|
||||||
{
|
{
|
||||||
if (!_notifierFolders.Add(folder.NodeId)) return;
|
if (!_notifierFolders.TryAdd(folder.NodeId, folder)) return;
|
||||||
folder.EventNotifier = EventNotifiers.SubscribeToEvents;
|
folder.EventNotifier = EventNotifiers.SubscribeToEvents;
|
||||||
AddRootNotifier(folder);
|
AddRootNotifier(folder);
|
||||||
folder.ClearChangeMasks(SystemContext, includeChildren: false);
|
folder.ClearChangeMasks(SystemContext, includeChildren: false);
|
||||||
@@ -240,7 +255,6 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
|||||||
/// <see cref="EventSeverity"/> enum buckets the SDK's <c>SetSeverity</c> expects.</summary>
|
/// <see cref="EventSeverity"/> enum buckets the SDK's <c>SetSeverity</c> expects.</summary>
|
||||||
private static EventSeverity MapSeverity(int severity) => severity switch
|
private static EventSeverity MapSeverity(int severity) => severity switch
|
||||||
{
|
{
|
||||||
<= 0 => EventSeverity.Low,
|
|
||||||
< 200 => EventSeverity.Low,
|
< 200 => EventSeverity.Low,
|
||||||
< 400 => EventSeverity.MediumLow,
|
< 400 => EventSeverity.MediumLow,
|
||||||
< 600 => EventSeverity.Medium,
|
< 600 => EventSeverity.Medium,
|
||||||
@@ -380,6 +394,15 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
|||||||
}
|
}
|
||||||
_folders.Clear();
|
_folders.Clear();
|
||||||
|
|
||||||
|
// Detach the Server↔folder HasNotifier ref for every promoted folder before dropping the
|
||||||
|
// guard, otherwise the rebuild leaks an orphaned root-notifier reference on the Server
|
||||||
|
// object. RemoveRootNotifier just severs that link, so its order relative to the folder
|
||||||
|
// teardown above doesn't matter — but it must run under this same Lock.
|
||||||
|
foreach (var folder in _notifierFolders.Values)
|
||||||
|
{
|
||||||
|
RemoveRootNotifier(folder);
|
||||||
|
}
|
||||||
|
|
||||||
// Drop the notifier-folder guard so re-materialised alarms re-promote their (rebuilt)
|
// Drop the notifier-folder guard so re-materialised alarms re-promote their (rebuilt)
|
||||||
// equipment folders to event notifiers.
|
// equipment folders to event notifiers.
|
||||||
_notifierFolders.Clear();
|
_notifierFolders.Clear();
|
||||||
|
|||||||
Reference in New Issue
Block a user