fix(opcua): stale-cut clear keeps newer episodes, dispose barrier + timeout warning, replay marker cleared on throw (#854 re-review)

This commit is contained in:
Joseph Doherty
2026-11-11 07:39:25 -05:00
parent fe48458a41
commit a0e9a45565
@@ -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();
}
}
}
}
/// <summary>