137 lines
9.4 KiB
C#
137 lines
9.4 KiB
C#
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
|
|
|
/// <summary>One walked-back reverse raw page (<see cref="ReverseRawReader"/>).</summary>
|
|
/// <param name="Samples">At most the requested cap, newest first.</param>
|
|
/// <param name="WidthHintTicks">The segment width the walk ended on — the next page's starting width (0 = none).</param>
|
|
internal readonly record struct ReverseRawPage(IReadOnlyList<DataValueSnapshot> Samples, long WidthHintTicks)
|
|
{
|
|
/// <summary>Everything the next page of the same read should resume with (its width is <see cref="WidthHintTicks"/>).</summary>
|
|
public ReverseWalkHint Hint { get; init; } = new(WidthHintTicks);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gitea #686: serves a Part 11 reverse raw read — <c>(lo, hi]</c> newest-first, capped — on a historian backend that can
|
|
/// only read forward (<see cref="IHistorianDataSource.SupportsReverseRaw"/> = <c>false</c>: the Gateway, whose
|
|
/// <c>ReadRaw</c> streams ascending and rejects <c>start > end</c>). The walk itself — segment sizing (#689), the
|
|
/// truncation rules, edges, ties and bounds — is <see cref="ReverseWalk"/>; this adapter supplies the raw read (a read is
|
|
/// cut short when it returned its full cap) and keeps the backend's tie order.
|
|
/// </summary>
|
|
internal static class ReverseRawReader
|
|
{
|
|
/// <summary>The first segment's width when no hint is carried.</summary>
|
|
internal static readonly TimeSpan InitialSegment = ReverseWalk.InitialSegment;
|
|
|
|
/// <summary>The walk's exclusive lower limit.</summary>
|
|
internal static readonly DateTime Floor = ReverseWalk.Floor;
|
|
|
|
/// <summary>How far past "now" the walk starts at most.</summary>
|
|
internal static readonly TimeSpan LookAhead = ReverseWalk.LookAhead;
|
|
|
|
/// <summary>Reads <c>(lo, hi]</c> newest-first from forward reads.</summary>
|
|
/// <param name="source">A backend that reads forward.</param>
|
|
/// <param name="tagname">Historian tag.</param>
|
|
/// <param name="hi">The later (inclusive) bound — the request's StartTime or a reverse resume point.</param>
|
|
/// <param name="lo">The earlier (exclusive) bound.</param>
|
|
/// <param name="maxValues">Cap; <c>0</c> = every sample in the window (one forward read).</param>
|
|
/// <param name="maxTieCluster">The tie budget (<c>MaxTieClusterOverfetch</c>; values below 1 count as 1) — a ceiling only.</param>
|
|
/// <param name="nowUtc">Now, for the look-ahead clamp.</param>
|
|
/// <param name="widthHintTicks">Starting segment width (the previous page's <see cref="ReverseRawPage.WidthHintTicks"/>), 0 = none.</param>
|
|
/// <param name="ct">Cancellation.</param>
|
|
/// <returns>The page, newest first, and the width to start the next page with.</returns>
|
|
/// <exception cref="HistoryReadLimitExceededException">One timestamp holds more than the tie budget.</exception>
|
|
public static Task<ReverseRawPage> 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);
|
|
|
|
/// <summary>Reads <c>(lo, hi]</c> newest-first from forward reads, resuming with the previous page's full hint.</summary>
|
|
/// <param name="source">A backend that reads forward.</param>
|
|
/// <param name="tagname">Historian tag.</param>
|
|
/// <param name="hi">The later (inclusive) bound.</param>
|
|
/// <param name="lo">The earlier (exclusive) bound.</param>
|
|
/// <param name="maxValues">Cap; <c>0</c> = every sample in the window (one forward read).</param>
|
|
/// <param name="maxTieCluster">The tie budget — a ceiling only.</param>
|
|
/// <param name="nowUtc">Now, for the look-ahead clamp.</param>
|
|
/// <param name="hint">What the previous page carried (<see cref="ReverseRawPage.Hint"/>), default = none.</param>
|
|
/// <param name="ct">Cancellation.</param>
|
|
/// <returns>The page, newest first, and the hint for the next page.</returns>
|
|
/// <exception cref="HistoryReadLimitExceededException">One timestamp holds more than the tie budget.</exception>
|
|
/// <remarks>
|
|
/// Gitea #694: every segment read fits the backend's <see cref="IHistorianDataSource.MaxRawValuesPerRead"/> (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 <paramref name="retryOnLearnedLimit"/> is false: the HistoryRead page path passes false, so the
|
|
/// refusal reaches <see cref="RawPageLimit"/>, 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 (<see cref="HistoryReadLimitExceededException.PreWindowRows"/>) 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.
|
|
/// </remarks>
|
|
/// <param name="retryOnLearnedLimit">Re-run the walk once when it taught the backend its limit (see the remarks).</param>
|
|
public static async Task<ReverseRawPage> 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<ReverseRawPage> 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<DataValueSnapshot> samples;
|
|
for (var attempt = 0; ; attempt++)
|
|
{
|
|
try
|
|
{
|
|
samples = (await source.ReadRawAsync(tagname, start, end, (uint)asked, token).ConfigureAwait(false)).Samples;
|
|
break;
|
|
}
|
|
catch (HistoryReadLimitExceededException ex) when (asked > 0 && ex.PreWindowRows > 0 && attempt < 2)
|
|
{
|
|
// #695: the discovery request may be smaller than the pre-window cluster. Size against
|
|
// the ceiling, not that request; a second pass can discover more crowding rows.
|
|
var ceilingRoom = source.MaxRawValuesPerRead is var learned and > 0 ? learned : asked;
|
|
var smaller = Math.Min(asked - 1, ceilingRoom - ex.PreWindowRows - 1);
|
|
if (smaller < 1) throw;
|
|
asked = smaller;
|
|
}
|
|
}
|
|
return new ForwardSegmentRead<DataValueSnapshot>(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, useDiscoveryBudgetForGaps: limit > 0)
|
|
.ConfigureAwait(false);
|
|
return new ReverseRawPage(rows, next.WidthTicks) { Hint = next };
|
|
}
|
|
}
|
|
|
|
private static long Ticks(DataValueSnapshot s) => (s.SourceTimestampUtc ?? DateTime.MinValue).Ticks;
|
|
}
|