From c1e921de0bdc71022080ef21bf0ff15fdc63eafc Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 14 Jun 2026 01:39:47 -0400 Subject: [PATCH] fix(opcua): RunContinuationsAsynchronously so revert never re-enters the write Lock --- .../ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index 08ebd83d..3240e5cf 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -638,7 +638,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // Fire-and-forget — MUST NOT block under Lock. On a FAILED outcome, compare-and-revert (off-Lock // continuation). A faulted/cancelled WriteAsync is treated as a failure so the optimistic value never - // sticks when the route never resolved a real outcome. + // sticks when the route never resolved a real outcome. RunContinuationsAsynchronously guarantees the + // revert never runs inline on the SDK write thread (the gateway can return a synchronously-completed + // task — e.g. its boot-window "no DriverHostActor yet" branch), so RevertOptimisticWriteIfNeeded never + // re-enters lock (Lock) while CustomNodeManager2.Write still holds it. _ = gateway.WriteAsync(nodeKey, optimisticValue, CancellationToken.None) .ContinueWith( t => @@ -646,7 +649,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 var outcome = t.IsCompletedSuccessfully ? t.Result : new NodeWriteOutcome(false, "write dispatch faulted"); RevertOptimisticWriteIfNeeded(nodeKey, outcome, optimisticValue, priorValue, priorStatus); }, - CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); + CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.Default); return ServiceResult.Good; }