fix(historian): #691.3 review — remember a strict gateway's ceiling per data source; re-probe once when a bounded read comes back full

This commit is contained in:
Joseph Doherty
2026-10-12 16:50:32 -04:00
parent ab5030174a
commit 7b86059daf
7 changed files with 190 additions and 38 deletions
@@ -166,6 +166,7 @@ internal static class RawAggregateReader
/// <summary>Reads and evaluates one per-sample processed read.</summary>
/// <param name="client">The gateway client.</param>
/// <param name="ceiling">The data source's learned gateway row ceiling (#691.3).</param>
/// <param name="tag">Historian tag name.</param>
/// <param name="aggregate">A per-sample aggregate (<see cref="Serves"/>).</param>
/// <param name="plan">The bucket plan.</param>
@@ -175,7 +176,7 @@ internal static class RawAggregateReader
/// <returns>One value per interval, in the plan's order.</returns>
/// <exception cref="HistoryReadLimitExceededException">The window holds more than <paramref name="maxWindowRows"/> rows,
/// or the stream could not be read to completion within the requested row budget.</exception>
public static async Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(IHistorianGatewayClient client, string tag,
public static async Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(IHistorianGatewayClient client, GatewayRowCeiling ceiling, string tag,
HistoryAggregateType aggregate, BucketPlan plan, int maxWindowRows, DateTime now, CancellationToken ct)
{
if (plan.BucketCount == 0) return [];
@@ -185,8 +186,9 @@ internal static class RawAggregateReader
// A strict gateway that refuses the budget is re-asked once at the ceiling it names (#691.3; e.g. a ceiling below
// the 1,000,000 #688 clamps at, or an unbounded cap) — and a stream that fills that ceiling is never evaluated.
var requested = MaxValuesFor(maxWindowRows);
var (pass, maxValues) = await GatewayReadLimits.ReadWithCeilingRetryAsync(requested,
budget => ReadWindowAsync(client, tag, aggregate, plan, requestStart, budget, maxWindowRows, ct)).ConfigureAwait(false);
var (pass, maxValues) = await ceiling.ReadAsync(requested,
budget => ReadWindowAsync(client, tag, aggregate, plan, requestStart, budget, maxWindowRows, ct),
(r, budget) => !r.OverCap && r.Received >= budget).ConfigureAwait(false);
var (buckets, earliest, latest, received, overCap) = pass;
if (overCap) throw LimitExceeded(tag, maxWindowRows);
if (received >= maxValues) throw BudgetExhausted(tag, maxWindowRows, requested, maxValues);