fix(historian): #688 clamp the Gateway per-sample row budget at the gateway's default ceiling

The default MaxTimeWeightedRawRows (1,000,000) + 18 exceeded the gateway's
default MaxValuesCeiling (1,000,000), so with AllowStreamingBeyondCeiling=false
every Gateway per-sample aggregate read was refused. The allowance is now clamped
at 1,000,000 while the cap is at or under it (RawAggregateReader.
ClampToGatewayCeiling); a cap above it keeps its allowance. Raw reads are not
clamped (rebased onto #686).
This commit is contained in:
Joseph Doherty
2026-10-12 14:37:35 -04:00
parent 9e47532283
commit 6796ba4126
4 changed files with 69 additions and 12 deletions
@@ -47,8 +47,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
/// SQL backends' time-weighted reads use): past it the node fails with
/// <see cref="HistoryReadLimitExceededException"/> (⇒ <c>BadTooManyOperations</c>) once the stream — bounded by the
/// requested row budget — has been drained. The gateway is asked for at most <c>cap + 2 + </c>
/// <see cref="PreWindowAllowance"/> rows; a stream that delivers all of them cannot be proven complete and fails the
/// same way rather than answering from a truncated window.
/// <see cref="PreWindowAllowance"/> rows, clamped at <see cref="GatewayDefaultMaxValuesCeiling"/> while the cap is at
/// or under it (#688, <see cref="ClampToGatewayCeiling"/>); a stream that delivers all of them cannot be proven
/// complete and fails the same way rather than answering from a truncated window.
/// </para>
/// <para>
/// <b>Values and status.</b> A sample's value is its numeric value (Double), else its string value, else Empty; its
@@ -109,7 +110,29 @@ internal static class RawAggregateReader
/// <param name="maxWindowRows">The configured cap (&lt;= 0 = unbounded).</param>
/// <returns>The row limit to request.</returns>
public static int MaxValuesFor(int maxWindowRows) =>
maxWindowRows <= 0 ? int.MaxValue : (int)Math.Min(int.MaxValue, (long)maxWindowRows + 2 + PreWindowAllowance);
maxWindowRows <= 0 ? int.MaxValue : ClampToGatewayCeiling((long)maxWindowRows + 2 + PreWindowAllowance, maxWindowRows);
/// <summary>
/// The HistorianGateway's default <c>Historian:ReadLimits:MaxValuesCeiling</c>. With
/// <c>AllowStreamingBeyondCeiling=false</c> the gateway refuses any <c>max_values</c> above its ceiling.
/// </summary>
internal const int GatewayDefaultMaxValuesCeiling = 1_000_000;
/// <summary>
/// #688: a <c>ReadRaw</c> row budget of <paramref name="rows"/> plus this backend's own allowance
/// (<paramref name="requested"/>) clamped at <see cref="GatewayDefaultMaxValuesCeiling"/> — but never below
/// <paramref name="rows"/> itself. The allowance (the boundary row, the margin, the completeness row) must not push a
/// budget that fits the gateway's default ceiling over it: the default <c>MaxTimeWeightedRawRows</c> (1,000,000) + 18
/// used to exceed it, so a gateway with <c>AllowStreamingBeyondCeiling=false</c> refused EVERY per-sample read. A
/// count above the ceiling is the operator's (or client's) deliberate choice for a streaming gateway and keeps its
/// allowance. The cost of clamping is that a window holding within the allowance of the ceiling cannot be proven
/// complete and fails as over the cap — the same outcome it would get one row later.
/// </summary>
/// <param name="requested">The budget including the allowance.</param>
/// <param name="rows">The caller's own row count (the cap, or <c>NumValuesPerNode</c>), &gt; 0.</param>
/// <returns>The <c>max_values</c> to request.</returns>
internal static int ClampToGatewayCeiling(long requested, long rows) =>
(int)Math.Min(int.MaxValue, rows > GatewayDefaultMaxValuesCeiling ? requested : Math.Min(requested, GatewayDefaultMaxValuesCeiling));
/// <summary>Maps one gateway raw sample to a stored sample (value + mapped status, Empty clamped Bad).</summary>
/// <param name="sample">The gateway sample; its timestamp must be set.</param>