fix(historian): #686 review — bounded, density-aware reverse raw walk; uncapped Gateway raw reads
1. Uncapped reverse raw read on a forward-only backend = ONE forward read of the window, filtered and reversed in memory (was a segment-by-segment walk). 2. Segment width is aimed at ~half the row budget from the density just observed (overflow: from the truncated read's own sample span) and carried page to page in HistoryContinuationState.SegmentWidthTicks. 3. The walk starts no later than now + 1 day and never reads before the epoch. 4. Request bounds are rounded outward to the millisecond, so a row AVEVA fabricates at a rounded-up query start cannot land inside the segment. 5. Budget M = max(1, MaxTieClusterOverfetch); reads ask M + 2 rows, completeness is 'short OR last row past the segment', and a read crowded by rows before the segment is re-read once (<= 2M + 4). A cluster of exactly M, neighbouring clusters each within M, and M <= 1 are all served; only one timestamp over M is refused. ServerHistorianOptions text updated. 6. The raw and events tie-path oversized-cluster refusal is BadTooManyOperations (was BadHistoryOperationUnsupported), consistent with the walk. 7. GatewayHistorianDataSource.ReadRawAsync: a caller cancellation is not a backend failure; NumValuesPerNode = 0 is sent as int.MaxValue (the client package and the gateway reject max_values <= 0, so every uncapped Gateway raw read failed); a MaxValuesCeiling refusal is a HistoryReadLimitExceededException. 8. Tests: upper-edge filter, epoch floor, look-ahead, ms alignment, exact/adjacent clusters, minimal budget, carried density, and the walk over the real Gateway source + FakeHistorianGatewayClient (OpcUaServer internals visible to it).
This commit is contained in:
@@ -536,8 +536,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// cap. When a resume read stalls on such a cluster (the boundary-tie trim empties the page), the
|
||||
/// paging over-fetches up to <c>MaxTieClusterOverfetch + 1</c> ties at that single timestamp (a
|
||||
/// <c>start == end</c> read) and slices through them via <see cref="HistoryPaging.SliceTieCluster"/>.
|
||||
/// A cluster strictly larger than this still surfaces <c>BadHistoryOperationUnsupported</c> for that
|
||||
/// node (the absurd-burst backstop). Mirrors the configured
|
||||
/// A cluster strictly larger than this still fails that node <c>BadTooManyOperations</c> (the absurd-burst
|
||||
/// backstop; <c>BadHistoryOperationUnsupported</c> before #686). Also the per-read row budget of a reverse raw read
|
||||
/// walked back from forward reads (<see cref="ReverseRawReader"/>). Mirrors the configured
|
||||
/// <c>ServerHistorianOptions.MaxTieClusterOverfetch</c>; the Host sets it at <c>StartAsync</c>. The
|
||||
/// default (65536) survives when the historian section is absent.
|
||||
/// </summary>
|
||||
@@ -5359,8 +5360,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
"OtOpcUaNodeManager: HistoryReadEvents tie cluster at {StartUtc:O} for source '{Source}' has {Count} events, " +
|
||||
"exceeding MaxTieClusterOverfetch={Max}; cannot page within it. Increase MaxTieClusterOverfetch.",
|
||||
startUtc, sourceName, cluster.Count, MaxTieClusterOverfetch);
|
||||
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
|
||||
results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadHistoryOperationUnsupported };
|
||||
errors[handle.Index] = StatusCodes.BadTooManyOperations;
|
||||
results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadTooManyOperations };
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6183,6 +6184,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
DateTime startUtc;
|
||||
var boundarySkip = 0;
|
||||
long segmentWidth = 0; // #686: the walk-back's width, carried page to page
|
||||
|
||||
string tagname;
|
||||
if (inboundCp is { Length: > 0 })
|
||||
@@ -6203,6 +6205,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
boundarySkip = state.BoundarySkipCount;
|
||||
endUtc = state.EndUtc;
|
||||
numValuesPerNode = state.NumValuesPerNode;
|
||||
segmentWidth = state.SegmentWidthTicks;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -6235,16 +6238,22 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// can read newest-first; otherwise it is walked back from bounded forward reads (#686 — the Gateway rejects
|
||||
// start > end, which failed the node and counted as a backend fault).
|
||||
var source = HistorianDataSource;
|
||||
var sourceResult = startUtc > endUtc && !source.SupportsReverseRaw
|
||||
? new HistorianRead(
|
||||
await ReverseRawReader.ReadAsync(
|
||||
source, tagname, startUtc, endUtc, numValuesPerNode,
|
||||
(int)Math.Min(int.MaxValue, (long)Math.Max(1, MaxTieClusterOverfetch) + 1), ct)
|
||||
.ConfigureAwait(false),
|
||||
ContinuationPoint: null)
|
||||
: await source
|
||||
HistorianRead sourceResult;
|
||||
if (startUtc > endUtc && !source.SupportsReverseRaw)
|
||||
{
|
||||
var page = await ReverseRawReader.ReadAsync(
|
||||
source, tagname, startUtc, endUtc, numValuesPerNode, MaxTieClusterOverfetch,
|
||||
Clock.GetUtcNow().UtcDateTime, segmentWidth, ct)
|
||||
.ConfigureAwait(false);
|
||||
segmentWidth = page.WidthHintTicks;
|
||||
sourceResult = new HistorianRead(page.Samples, ContinuationPoint: null);
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceResult = await source
|
||||
.ReadRawAsync(tagname, startUtc, endUtc, numValuesPerNode, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var backendFull = HistoryPaging.IsFullPage(sourceResult.Samples.Count, numValuesPerNode);
|
||||
|
||||
@@ -6293,8 +6302,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
"OtOpcUaNodeManager: HistoryReadRaw tie cluster at {StartUtc:O} for tag '{Tagname}' has {Count} samples, " +
|
||||
"exceeding MaxTieClusterOverfetch={Max}; cannot page within it. Increase MaxTieClusterOverfetch.",
|
||||
startUtc, tagname, cluster.Count, MaxTieClusterOverfetch);
|
||||
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
|
||||
results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadHistoryOperationUnsupported };
|
||||
errors[handle.Index] = StatusCodes.BadTooManyOperations;
|
||||
results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadTooManyOperations };
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6315,7 +6324,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
if (nextStartUtc is { } resumeAt)
|
||||
{
|
||||
var clusterState = new HistoryContinuationState(
|
||||
tagname, resumeAt, endUtc, nextSkip, numValuesPerNode);
|
||||
tagname, resumeAt, endUtc, nextSkip, numValuesPerNode, SegmentWidthTicks: segmentWidth);
|
||||
clusterCp = _historyContinuationStore.Save(session, clusterState);
|
||||
}
|
||||
|
||||
@@ -6338,7 +6347,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
HistoryPaging.ComputeResumeCursor(samples, out var nextStart, out var skip);
|
||||
var nextState = new HistoryContinuationState(
|
||||
tagname, nextStart, endUtc, skip, numValuesPerNode);
|
||||
tagname, nextStart, endUtc, skip, numValuesPerNode, SegmentWidthTicks: segmentWidth);
|
||||
// Save may return null (no session on this request) ⇒ degrade to single-shot for this node.
|
||||
// Built AFTER historyData so a failure projecting samples can never orphan a stored cursor.
|
||||
outboundCp = _historyContinuationStore.Save(session, nextState);
|
||||
|
||||
Reference in New Issue
Block a user