fix(historian): #691.2 review — forward native TimeAverage/Total on the SQL grid: clipped latest interval read over its exact span, Partial, interval clamped to the window

This commit is contained in:
Joseph Doherty
2026-10-12 16:55:01 -04:00
parent 7b86059daf
commit 830b47f624
8 changed files with 205 additions and 166 deletions
@@ -253,9 +253,9 @@ public sealed class GatewayHistorianDataSource : IHistorianDataSource, IAsyncDis
/// <para>
/// <b>TimeAverage / Total</b> stay native AVEVA retrieval modes (<see cref="AggregateModeMapper"/>), stamped with
/// the interval start and the Calculated bit; AVEVA's extra row for the interval before the window is dropped and
/// an Uncertain interval keeps its value (#685, <see cref="SampleMapper.ToAggregateSnapshots"/>). A reverse read
/// (<c>start &gt; end</c>, which the gateway refuses) is served by forward requests on the later-anchored #677 grid
/// (#691.2, <see cref="NativeReverseAggregatePlan"/>). Everything else is <see cref="HistoryAggregateNotSupportedException"/>
/// an Uncertain interval keeps its value (#685). Both directions use the SQL backends' interval grid with forward
/// requests only — a reverse read (which the gateway refuses) anchored at its later time (#677), the clipped interval
/// read over exactly its span and marked Partial (#691.2, <see cref="NativeAggregatePlan"/>). Everything else is <see cref="HistoryAggregateNotSupportedException"/>
/// (see <see cref="ServedAggregates"/>).
/// </para>
/// </remarks>
@@ -272,50 +272,23 @@ public sealed class GatewayHistorianDataSource : IHistorianDataSource, IAsyncDis
// Mapped OUTSIDE the failure-recording try: an aggregate the gateway has no mode for is a client
// request the server answers BadAggregateNotSupported, not a backend fault to count (#285).
var mode = AggregateModeMapper.ToRetrievalMode(aggregate);
if (startUtc > endUtc)
return await ReadNativeReverseAsync(fullReference, BucketPlan.Create(startUtc, endUtc, interval), mode, cancellationToken)
.ConfigureAwait(false);
try
{
// TimeAverage / Total are native gateway retrieval modes (TimeWeightedAverage / Integral) — no
// client-side scaling.
var buckets = new List<HistorianAggregateSample>();
await foreach (var bucket in _client
.ReadAggregateAsync(fullReference, startUtc, endUtc, mode, interval, cancellationToken)
.ConfigureAwait(false))
{
buckets.Add(bucket);
}
// Filtered while mapping, after the stream was drained (#685: AVEVA's extra pre-window row).
var snapshots = SampleMapper.ToAggregateSnapshots(buckets, startUtc, endUtc);
RecordOutcome(success: true, error: null);
return new HistoryReadResult(snapshots, ContinuationPoint: null);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw; // caller-requested, not a backend fault
}
catch (Exception ex)
{
RecordReadFailure(ex);
throw;
}
// Planned OUTSIDE the failure-recording try: too many intervals is a client request limit, not a backend fault.
return await ReadNativeAsync(fullReference, BucketPlan.Create(startUtc, endUtc, interval), mode, cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// A reverse (<c>start &gt; end</c>) native TimeAverage / Total read (#691.2), anchored at its later time as on the SQL
/// backends (#677) — the gateway refuses <c>start &gt; end</c>, so it is served by forward native requests on the
/// later-anchored grid. See <see cref="NativeReverseAggregatePlan"/>.
/// A native TimeAverage / Total read on the SQL backends' interval grid, either direction (#691.2): forward native
/// requests only, the clipped interval read over exactly its span. See <see cref="NativeAggregatePlan"/>.
/// </summary>
private async Task<HistoryReadResult> ReadNativeReverseAsync(string fullReference, BucketPlan plan, RetrievalMode mode,
private async Task<HistoryReadResult> ReadNativeAsync(string fullReference, BucketPlan plan, RetrievalMode mode,
CancellationToken cancellationToken)
{
if (plan.BucketCount == 0) return new HistoryReadResult([], ContinuationPoint: null);
try
{
var byBucket = new Dictionary<long, DataValueSnapshot>();
foreach (var request in NativeReverseAggregatePlan.Requests(plan))
foreach (var request in NativeAggregatePlan.Requests(plan))
{
var buckets = new List<HistorianAggregateSample>();
await foreach (var bucket in _client
@@ -324,11 +297,11 @@ public sealed class GatewayHistorianDataSource : IHistorianDataSource, IAsyncDis
{
buckets.Add(bucket); // drained to completion, mapped afterwards
}
NativeReverseAggregatePlan.Place(plan, request, buckets, byBucket);
NativeAggregatePlan.Place(plan, request, buckets, byBucket);
}
RecordOutcome(success: true, error: null);
return new HistoryReadResult(NativeReverseAggregatePlan.Assemble(plan, byBucket), ContinuationPoint: null);
return new HistoryReadResult(NativeAggregatePlan.Assemble(plan, byBucket), ContinuationPoint: null);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{