fix(opcua): event paging review fixes (#669): open-ended end, node-bound cursors, save after projection
This commit is contained in:
@@ -5155,7 +5155,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// Single-use take. A miss (unknown / evicted / malformed / a RAW cursor) ⇒ BadContinuationPointInvalid
|
||||
// for THIS node only.
|
||||
var state = _historyContinuationStore.TryTake(session, inboundCp!);
|
||||
if (state is null || !state.IsEvents)
|
||||
// A point is bound to the node that issued it: another node's source name is a miss too.
|
||||
if (state is null || !state.IsEvents || state.Tagname != (sourceName ?? string.Empty))
|
||||
{
|
||||
errors[handle.Index] = StatusCodes.BadContinuationPointInvalid;
|
||||
results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadContinuationPointInvalid };
|
||||
@@ -5169,8 +5170,13 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
numValuesPerNode = state.NumValuesPerNode;
|
||||
}
|
||||
|
||||
// Part 11: an unset EndTime (MinValue) with a StartTime means "page forward from StartTime". Normalise it
|
||||
// to an open upper bound, or the cursor would store MinValue and a backend would swap the reversed
|
||||
// resume window. A resumed read is always forward (the cursor is only ever stored for a forward read),
|
||||
// so its direction comes from the cursor, not from the resume request's details.
|
||||
if (!resumed && endUtc == DateTime.MinValue && startUtc != DateTime.MinValue) endUtc = DateTime.MaxValue;
|
||||
// Reverse event reads (start > end) are not paged: the cursor below is forward-only.
|
||||
var forward = !(details.EndTime != DateTime.MinValue && details.StartTime > details.EndTime);
|
||||
var forward = resumed || startUtc <= endUtc;
|
||||
|
||||
// NOT under the node-manager Lock — awaiting the async source is safe here.
|
||||
// NumValuesPerNode==0 means "no limit — return ALL values" per OPC UA Part 4/11, so it is translated
|
||||
@@ -5191,7 +5197,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
if (i > 0) events = events.Skip(i).ToList();
|
||||
}
|
||||
|
||||
byte[]? outboundCp = null;
|
||||
// The cursor to store, if any. Saved only AFTER the page is projected, so a projection throw can
|
||||
// never orphan a stored cursor.
|
||||
HistoryContinuationState? pendingCursor = null;
|
||||
|
||||
if (truncated && resumed
|
||||
&& (events.Count == 0 || events[^1].EventTimeUtc == startUtc))
|
||||
@@ -5227,8 +5235,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
|
||||
if (nextStartUtc is { } resumeAt)
|
||||
{
|
||||
outboundCp = _historyContinuationStore.Save(session, new HistoryContinuationState(
|
||||
sourceName ?? string.Empty, resumeAt, endUtc, nextSkip, numValuesPerNode, IsEvents: true));
|
||||
pendingCursor = new HistoryContinuationState(
|
||||
sourceName ?? string.Empty, resumeAt, endUtc, nextSkip, numValuesPerNode, IsEvents: true);
|
||||
}
|
||||
}
|
||||
else if (truncated && events.Count > 0)
|
||||
@@ -5237,11 +5245,12 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
var boundary = events[^1].EventTimeUtc;
|
||||
var skip = 0;
|
||||
for (var i = events.Count - 1; i >= 0 && events[i].EventTimeUtc == boundary; i--) skip++;
|
||||
outboundCp = _historyContinuationStore.Save(session, new HistoryContinuationState(
|
||||
sourceName ?? string.Empty, boundary, endUtc, skip, numValuesPerNode, IsEvents: true));
|
||||
pendingCursor = new HistoryContinuationState(
|
||||
sourceName ?? string.Empty, boundary, endUtc, skip, numValuesPerNode, IsEvents: true);
|
||||
}
|
||||
|
||||
var historyEvent = ProjectEvents(events, selectClauses);
|
||||
var outboundCp = pendingCursor is null ? null : _historyContinuationStore.Save(session, pendingCursor);
|
||||
results[handle.Index] = new SdkHistoryReadResult
|
||||
{
|
||||
// No events ⇒ GoodNoData (the notifier is historized, the window just held no events).
|
||||
@@ -5932,7 +5941,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// Resume read: take the stored cursor. A miss (unknown / evicted / malformed point) ⇒
|
||||
// BadContinuationPointInvalid for THIS node.
|
||||
var state = _historyContinuationStore.TryTake(session, inboundCp);
|
||||
if (state is null || state.IsEvents)
|
||||
if (state is null || state.IsEvents
|
||||
|| !TryGetHistorizedTagname(handle.NodeId, out var boundTag) || boundTag != state.Tagname)
|
||||
{
|
||||
errors[handle.Index] = StatusCodes.BadContinuationPointInvalid;
|
||||
results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadContinuationPointInvalid };
|
||||
|
||||
Reference in New Issue
Block a user