fix(otopcua): report NodeAdded model-change outside the node Lock

This commit is contained in:
Joseph Doherty
2026-06-26 07:06:43 -04:00
parent 93f7586590
commit f8406d348c
2 changed files with 46 additions and 15 deletions
@@ -1576,31 +1576,38 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// served Equipment address space (Tasks 4/5), an attribute notification alone is invisible to a
/// subscribed client — only a model-change event tells it the address space grew.
/// <para>
/// Built AND reported under <c>Lock</c> (like <see cref="ReportConditionEvent"/>) and wrapped in
/// try/catch so it is tolerant when eventing is disabled / there are no monitored items / the
/// server is shutting down — the same swallow-and-log tolerance as the write-revert path
/// (<see cref="ReportAuditEvent"/>). The nodes have already been materialised, so a surprise from
/// the event path MUST NOT propagate out of this announcement.
/// The event is built under <c>Lock</c> but reported AFTER the lock is released, mirroring
/// <see cref="ReportNodeShapeChangedEvent"/> / <see cref="RevertOptimisticWriteIfNeeded"/>:
/// <c>Server.ReportEvent</c> re-enters the server's own subscription/event path, so holding the node
/// <c>Lock</c> across it risks a lock-order inversion with a client that has event subscriptions.
/// The report is wrapped in try/catch so it is tolerant when eventing is disabled / there are no
/// monitored items / the server is shutting down — the same swallow-and-log tolerance as the
/// write-revert path (<see cref="ReportAuditEvent"/>). The nodes have already been materialised, so
/// a surprise from the event path MUST NOT propagate out of this announcement.
/// </para>
/// </summary>
/// <param name="affectedNodeId">The folder-scoped node id of the parent under which nodes were added.</param>
public void RaiseNodesAddedModelChange(string affectedNodeId)
{
ArgumentException.ThrowIfNullOrEmpty(affectedNodeId);
GeneralModelChangeEventState e;
lock (Lock)
{
try
{
Server.ReportEvent(SystemContext, BuildNodesAddedModelChange(affectedNodeId));
}
catch (Exception ex)
{
// Model-change reporting disabled / no monitored items / server shutting down ⇒ ReportEvent may
// no-op or throw; either way the node add already stands. Log to the SDK trace, don't rethrow.
e = BuildNodesAddedModelChange(affectedNodeId);
}
// Report OUTSIDE Lock — Server.ReportEvent re-enters the server's own subscription/event path; holding
// Lock across it risks a lock-order inversion (mirrors ReportNodeShapeChangedEvent).
try
{
Server.ReportEvent(SystemContext, e);
}
catch (Exception ex)
{
// Model-change reporting disabled / no monitored items / server shutting down ⇒ ReportEvent may
// no-op or throw; either way the node add already stands. Log to the SDK trace, don't rethrow.
#pragma warning disable CS0618 // Utils.LogError is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry.
Utils.LogError(ex, "OtOpcUaNodeManager: failed to report GeneralModelChangeEvent(NodeAdded) for {0}", affectedNodeId);
Utils.LogError(ex, "OtOpcUaNodeManager: failed to report GeneralModelChangeEvent(NodeAdded) for {0}", affectedNodeId);
#pragma warning restore CS0618
}
}
}