diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index 95ceb77d4..050202a84 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -624,7 +624,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2, INodeManager3 // #854 review: refuse new Server-object refreshes and let the ones in flight finish (the base's private // NodeManagerOperation guard does this for its own paths). _disposing = true; - SpinWait.SpinUntil(() => Volatile.Read(ref _refreshesInFlight) == 0, TimeSpan.FromSeconds(10)); + Interlocked.MemoryBarrier(); // the flag write must not be reordered after the in-flight read (#854 re-review) + if (!SpinWait.SpinUntil(() => Volatile.Read(ref _refreshesInFlight) == 0, TimeSpan.FromSeconds(10))) + _logger.LogWarning("Node manager disposed with {Count} Server-object ConditionRefresh call(s) still in flight after 10 s", + Volatile.Read(ref _refreshesInFlight)); Volatile.Read(ref _dataPlaneAuthority).Changed -= OnAuthorityChanged; _liveItems.Clear(); _eventItems.Clear(); @@ -1812,7 +1815,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2, INodeManager3 if (!_allEventsItemStates.TryGetValue(item, out var state)) continue; lock (state.Gate) { - if (state.Cut is { } cut && cut.Episode != current) state.Cut = null; + if (state.Cut is { } cut && cut.Episode < current) state.Cut = null; // episodes only grow: never clear a newer cut (#854 re-review) } } } @@ -6472,6 +6475,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2, INodeManager3 var states = items.Select(AllEventsItemStateOf).ToList(); var perItem = new List<(IFilterTarget Event, AlarmConditionState Condition)>[items.Count]; var generation = Interlocked.Increment(ref _refreshGeneration); + // #854 re-review: a throw between installing the "replay pending" markers and finishing the replay must not leave + // a marker set (every sync event to that item would keep taking the gate until its next refresh). + try + { lock (Lock) { var watermark = Interlocked.Read(ref _conditionEventSequence); @@ -6560,6 +6567,19 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2, INodeManager3 } } } + } + finally + { + foreach (var state in states) + { + lock (state.Gate) + { + if (state.PendingReplay != generation) continue; + state.PendingReplay = 0; + state.NewerSinceCut.Clear(); + } + } + } } ///