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); } }