fix(historian): #691.2 serve reverse native TimeAverage/Total on Gateway via forward reads on the later-anchored #677 grid

This commit is contained in:
Joseph Doherty
2026-10-12 16:17:05 -04:00
parent dbffb6bdb4
commit 0e47e887de
7 changed files with 349 additions and 6 deletions
@@ -249,7 +249,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"/>). Everything else is <see cref="HistoryAggregateNotSupportedException"/>
/// 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"/>
/// (see <see cref="ServedAggregates"/>).
/// </para>
/// </remarks>
@@ -266,6 +268,9 @@ 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
@@ -294,6 +299,44 @@ public sealed class GatewayHistorianDataSource : IHistorianDataSource, IAsyncDis
}
}
/// <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"/>.
/// </summary>
private async Task<HistoryReadResult> ReadNativeReverseAsync(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))
{
var buckets = new List<HistorianAggregateSample>();
await foreach (var bucket in _client
.ReadAggregateAsync(fullReference, request.StartUtc, request.EndUtc, mode, request.Interval, cancellationToken)
.ConfigureAwait(false))
{
buckets.Add(bucket); // drained to completion, mapped afterwards
}
NativeReverseAggregatePlan.Place(plan, request, buckets, byBucket);
}
RecordOutcome(success: true, error: null);
return new HistoryReadResult(NativeReverseAggregatePlan.Assemble(plan, byBucket), ContinuationPoint: null);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw; // caller-requested, not a backend fault
}
catch (Exception ex)
{
RecordReadFailure(ex);
throw;
}
}
/// <summary>Serves a per-sample aggregate from a raw read (#681); see <see cref="RawAggregateReader"/>.</summary>
private async Task<HistoryReadResult> ReadProcessedFromRawAsync(
string fullReference, DateTime startUtc, DateTime endUtc, TimeSpan interval,