using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// One walked-back reverse raw page ().
/// At most the requested cap, newest first.
/// The segment width the walk ended on — the next page's starting width (0 = none).
internal readonly record struct ReverseRawPage(IReadOnlyList Samples, long WidthHintTicks)
{
/// Everything the next page of the same read should resume with (its width is ).
public ReverseWalkHint Hint { get; init; } = new(WidthHintTicks);
}
///
/// Gitea #686: serves a Part 11 reverse raw read — (lo, hi] newest-first, capped — on a historian backend that can
/// only read forward ( = false: the Gateway, whose
/// ReadRaw streams ascending and rejects start > end). The walk itself — segment sizing (#689), the
/// truncation rules, edges, ties and bounds — is ; this adapter supplies the raw read (a read is
/// cut short when it returned its full cap) and keeps the backend's tie order.
///
internal static class ReverseRawReader
{
/// The first segment's width when no hint is carried.
internal static readonly TimeSpan InitialSegment = ReverseWalk.InitialSegment;
/// The walk's exclusive lower limit.
internal static readonly DateTime Floor = ReverseWalk.Floor;
/// How far past "now" the walk starts at most.
internal static readonly TimeSpan LookAhead = ReverseWalk.LookAhead;
/// Reads (lo, hi] newest-first from forward reads.
/// A backend that reads forward.
/// Historian tag.
/// The later (inclusive) bound — the request's StartTime or a reverse resume point.
/// The earlier (exclusive) bound.
/// Cap; 0 = every sample in the window (one forward read).
/// The tie budget (MaxTieClusterOverfetch; values below 1 count as 1) — a ceiling only.
/// Now, for the look-ahead clamp.
/// Starting segment width (the previous page's ), 0 = none.
/// Cancellation.
/// The page, newest first, and the width to start the next page with.
/// One timestamp holds more than the tie budget.
public static Task ReadAsync(
IHistorianDataSource source, string tagname, DateTime hi, DateTime lo, uint maxValues, int maxTieCluster,
DateTime nowUtc, long widthHintTicks, CancellationToken ct) =>
ReadAsync(source, tagname, hi, lo, maxValues, maxTieCluster, nowUtc, new ReverseWalkHint(widthHintTicks), ct);
/// Reads (lo, hi] newest-first from forward reads, resuming with the previous page's full hint.
/// A backend that reads forward.
/// Historian tag.
/// The later (inclusive) bound.
/// The earlier (exclusive) bound.
/// Cap; 0 = every sample in the window (one forward read).
/// The tie budget — a ceiling only.
/// Now, for the look-ahead clamp.
/// What the previous page carried (), default = none.
/// Cancellation.
/// The page, newest first, and the hint for the next page.
/// One timestamp holds more than the tie budget.
///
/// Gitea #694: every segment read fits the backend's (a strict
/// gateway's learned ceiling) — the walk's tie budget is bounded by it so its segments are sized for it, and a
/// crowded re-read asking more is cut to it (and counts as truncated, never as complete). A walk that TEACHES the
/// backend its limit (the first read after start-up) and is refused for it is re-run once with segments sized for the
/// new limit — unless is false: the HistoryRead page path passes false, so the
/// refusal reaches , which shrinks the PAGE to the limit as well (re-running the walk for a
/// page of N far above the limit cost thousands of segment reads). The bound lookups (a page of one) keep the re-run.
/// The page size itself is never cut here: a short answer would read as the last page. A segment refused because rows
/// just before it crowded the backend's budget () is
/// re-read once that many rows smaller and counts as truncated when full. An uncapped walk (one forward read of the
/// whole window) is never cut: it is answered in full or refused.
///
/// Re-run the walk once when it taught the backend its limit (see the remarks).
public static async Task ReadAsync(
IHistorianDataSource source, string tagname, DateTime hi, DateTime lo, uint maxValues, int maxTieCluster,
DateTime nowUtc, ReverseWalkHint hint, CancellationToken ct, bool retryOnLearnedLimit = true)
{
var before = source.MaxRawValuesPerRead;
try
{
return await WalkAsync(before).ConfigureAwait(false);
}
catch (HistoryReadLimitExceededException) when (retryOnLearnedLimit && maxValues > 0
&& source.MaxRawValuesPerRead is var now and > 0 && now != before)
{
// fall through (outside the catch): the walk taught the backend its limit — re-run it sized for that limit once.
// Not an uncapped walk: it is one read of the whole window, which the limit does not change.
}
return await WalkAsync(source.MaxRawValuesPerRead).ConfigureAwait(false);
async Task WalkAsync(int limit)
{
// A segment asks for at most its budget + 2 rows (ReverseWalk), so the budget is bounded 2 under the limit.
var budget = Math.Max(1, maxTieCluster);
if (limit > 0) budget = Math.Min(budget, Math.Max(1, limit - 2));
var (rows, next) = await ReverseWalk.WalkAsync(
async (start, end, cap, token) =>
{
var asked = limit > 0 && cap > limit ? limit : cap; // cap 0 (the whole window) is never cut
IReadOnlyList samples;
try
{
samples = (await source.ReadRawAsync(tagname, start, end, (uint)asked, token).ConfigureAwait(false)).Samples;
}
catch (HistoryReadLimitExceededException ex) when (asked > 0 && ex.PreWindowRows > 0 && asked - ex.PreWindowRows >= 1)
{
// #694: rows just before the segment crowded the backend's budget — read it smaller; a full answer
// then counts as truncated (the walk narrows), never as complete.
asked -= ex.PreWindowRows;
samples = (await source.ReadRawAsync(tagname, start, end, (uint)asked, token).ConfigureAwait(false)).Samples;
}
return new ForwardSegmentRead(samples, asked > 0 && samples.Count >= asked);
},
Ticks, tieOrder: null, hi, lo, maxValues, budget, source.TimestampResolutionTicks, nowUtc, hint,
at => new HistoryReadLimitExceededException(budget,
$"A reverse raw read of '{tagname}' at {at:O} found more than {budget} samples within one millisecond; the " +
"backend reads forward only, so the latest of them cannot be selected. Increase " +
(budget < Math.Max(1, maxTieCluster)
? "the backend's row ceiling (HistorianGateway Historian:ReadLimits:MaxValuesCeiling)."
: "ServerHistorian:MaxTieClusterOverfetch.")),
ct)
.ConfigureAwait(false);
return new ReverseRawPage(rows, next.WidthTicks) { Hint = next };
}
}
private static long Ticks(DataValueSnapshot s) => (s.SourceTimestampUtc ?? DateTime.MinValue).Ticks;
}