using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// How one HistoryRead-Raw page reads ().
/// The backend's the plan was made under (0 = none).
/// The page: at most this many values are returned (0 = all values — only without a limit).
/// What the read asks for: page + the boundary ties it will trim + one lookahead row (0 = all).
/// The boundary ties alone fill a read — nothing can be read past them; serve the tie cluster.
internal readonly record struct RawPagePlan(int Limit, uint PageCap, uint RequestCap, bool NoRoom);
///
/// Gitea #694: sizes a HistoryRead-Raw page to what the backend can return in one read
/// ( = L — a strict HistorianGateway's learned row ceiling, less
/// its boundary-row allowance). With a limit the read — page P + boundary ties k + the #692 lookahead row — is at most L,
/// so P = min(want, L − k − 1) (all values: P = L − k − 1); a full page carries a continuation point like any other, as
/// Part 11 allows fewer values than NumValuesPerNode with one. Before #694 a cap above a strict gateway's ceiling
/// (or an uncapped read of a window holding more) failed BadTooManyOperations.
///
///
///
/// Never a silent truncation. The backend still answers a read in full or refuses it; only the server's page is
/// smaller, and a page is judged full against what was asked for. A read the backend cannot serve at all (a tie
/// cluster larger than one read) stays BadTooManyOperations.
///
///
/// One re-read, planned again. A refused read () is re-planned and
/// re-issued ONCE when (a) the backend learned a different limit while it ran — the first read after start-up teaches
/// the ceiling — or (b) it was refused because rows before the window crowded the budget
/// ( m): the page is then shrunk by m so that the
/// backend's look-back fits beside it. Anything else is the answer.
///
///
internal static class RawPageLimit
{
/// Plans one page.
/// The page the client asked for (N, less a start bound; 0 = all values).
/// Ties at the resume timestamp already returned, which the read re-reads and trims (k).
/// The backend's limit L (<= 0 = none).
/// The limit to size the page by (L, or L − m after a crowded refusal); defaults to .
/// The plan.
public static RawPagePlan Plan(uint want, int boundaryTies, int limit, int? sizeLimit = null)
{
var k = Math.Max(0, boundaryTies);
if (limit <= 0)
return new RawPagePlan(0, want, want == 0 ? 0u : (uint)Math.Min(uint.MaxValue, (long)want + k + 1L), false);
var room = (long)(sizeLimit ?? limit) - k - 1;
if (room < 1) return new RawPagePlan(limit, 0, 0, true);
var page = want == 0 ? room : Math.Min(want, room);
return new RawPagePlan(limit, (uint)page, (uint)(page + k + 1), false);
}
///
/// Reads one page under the backend's current limit, re-planning and re-reading ONCE as described in the remarks.
/// A plan is returned without a read.
///
/// The page.
/// The backend.
/// The page the client asked for (0 = all values).
/// The boundary ties the read re-reads and trims.
/// One read with the given plan (it asks for ).
/// The page (null when ) and the plan it was read with.
public static async Task<(T? Page, RawPagePlan Plan)> ReadAsync(
IHistorianDataSource source, uint want, int boundaryTies, Func> read)
{
var before = source.MaxRawValuesPerRead;
var plan = Plan(want, boundaryTies, before);
if (plan.NoRoom) return (default, plan);
HistoryReadLimitExceededException refused;
try
{
return (await read(plan).ConfigureAwait(false), plan);
}
catch (HistoryReadLimitExceededException ex) when (source.MaxRawValuesPerRead is var now and > 0
&& (now != before || ex.PreWindowRows > 0))
{
refused = ex; // re-read outside the catch: the second refusal is the answer
}
var limit = source.MaxRawValuesPerRead;
var shrink = Math.Max(0, refused.PreWindowRows);
plan = Plan(want, boundaryTies, limit, (int)Math.Max(0L, (long)limit - shrink));
// No room left once the crowding rows are allowed for: those rows are not (necessarily) the boundary ties, so this is
// NOT "the cursor sits in a tie cluster" — the refusal is the answer (stepping the cursor through it a tick per page
// would never end).
// (A limit that changed under an un-crowded read leaves the boundary ties alone filling a read: that IS the tie
// cluster at the cursor, served by the over-fetch.)
if (plan.NoRoom)
{
if (shrink > 0) throw refused;
return (default, plan);
}
return (await read(plan).ConfigureAwait(false), plan);
}
}