fix(historian): refuse aggregates not valid for the node's DataType up front (#897)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
TimeAverage / Total on a Boolean node over the Gateway backend (AVEVA discrete tag, live SysPulse) went to the gateway since native TimeAverage/Total became default-on (#694). AVEVA refuses both modes at StartQuery; the gateway reports that only as Internal / HistorianServerOperationException (the same answer as a historian storage fault), so the node answered BadHistoryOperationUnsupported and the read counted as a backend failure. HistoryRead-Processed now checks the node's scalar DataType before calling any backend and answers BadAggregateNotSupported (Part 11 6.5.4: "If the Aggregate is not valid for the Node then the StatusCode shall be Bad_AggregateNotSupported"). Nothing is sent, nothing is counted. - Shared, every backend: HistoryAggregateDataTypes (Core.Abstractions) applies Part 13 5.4.2.3 Table 50: the numeric-only aggregates (Average, TimeAverage(2), Total(2), Min/Max, ActualTime, Range, Delta, DeltaBounds, Interpolative, stddev/variance) on a String or DateTime node are refused. Client-visible: the SQL backends used to answer every interval BadAggregateInvalidInputs. - Per backend: IHistorianDataSource.SupportsAggregateFor(aggregate, TypeCode?) (default true; every production backend declares it, guard-tested). Gateway: false for native TimeAverage/Total on a Boolean node. SqlServer/QuestDb: true, since they serve Boolean as 0/1 (stepped), so Boolean stays served there, as do the Gateway's raw-evaluated aggregates. The residual case (a node declared numeric whose AVEVA tag is discrete) cannot be told apart from a historian fault on the wire and still counts; that needs the gateway to surface AVEVA's native error code as a deterministic status. Tests: HistoryAggregateDataTypesTests, guard test, node-manager String/DateTime and per-type refusals, Gateway capability matrix, DiscreteTagTimeWeightedWireTests (production Gateway backend over an AVEVA-shaped fake: unsent, 0 health failures, positive controls). Each mutation (no check, no shared rule, no backend hint, Gateway hint true, shared rule always true, SQL inheriting the default) fails at least one. Live gate re-run read-only against AVEVA 2023 R2 via a private HistorianGateway fa9cf31 container: SysPulse TimeAverage on the wire = BadAggregateNotSupported, health failures unchanged; #801 gate 244 cases pass. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -722,6 +722,15 @@ public sealed class GatewayHistorianDataSource : IHistorianDataSource, IAsyncDis
|
||||
public IReadOnlySet<HistoryAggregateType> SupportedAggregates =>
|
||||
_enableNativeTimeWeighted ? ServedAggregates : RawEvaluatedAggregates;
|
||||
|
||||
/// <inheritdoc />
|
||||
// false for AVEVA's native TimeAverage / Total on a Boolean node (lmxopcua#897): AVEVA refuses both retrieval modes on a
|
||||
// discrete tag at StartQuery, and the gateway answers that only as Internal / HistorianServerOperationException — the
|
||||
// same answer as a historian storage fault — so it read as BadHistoryOperationUnsupported and a backend failure. The
|
||||
// server now answers BadAggregateNotSupported up front and nothing is sent. The raw-evaluated aggregates serve a
|
||||
// Boolean as 0/1 (as the SQL backends do); a String / DateTime node is refused by the shared HistoryAggregateDataTypes rule.
|
||||
public bool SupportsAggregateFor(HistoryAggregateType aggregate, TypeCode? valueType) =>
|
||||
!(valueType == TypeCode.Boolean && AggregateModeMapper.NativeAggregates.Contains(aggregate));
|
||||
|
||||
/// <inheritdoc />
|
||||
// Data + events, but NOT ServerTimestamps: AVEVA keeps no per-sample server time, so SampleMapper stamps the read
|
||||
// time — advertising ServerTimestampSupported would promise a stored value we do not have (lmxopcua#881).
|
||||
|
||||
@@ -438,6 +438,11 @@ public sealed class QuestDbHistorianDataSource : IHistorianDataSource
|
||||
/// <inheritdoc />
|
||||
public IReadOnlySet<HistoryAggregateType> SupportedAggregates => AggregateFamilies.Served;
|
||||
|
||||
/// <inheritdoc />
|
||||
// true for every type: a Boolean node's numeric aggregates are served as 0/1 (stepped for the time-weighted ones), and a
|
||||
// String / DateTime node's numeric ones are refused by the server's shared HistoryAggregateDataTypes rule before this.
|
||||
public bool SupportsAggregateFor(HistoryAggregateType aggregate, TypeCode? valueType) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
// Data + events; every sample row stores its server time and a raw read returns it (lmxopcua#881).
|
||||
public HistorianReadCapabilities ReadCapabilities =>
|
||||
|
||||
+5
@@ -363,6 +363,11 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
/// <inheritdoc />
|
||||
public IReadOnlySet<HistoryAggregateType> SupportedAggregates => AggregateFamilies.Served;
|
||||
|
||||
/// <inheritdoc />
|
||||
// true for every type: a Boolean node's numeric aggregates are served as 0/1 (stepped for the time-weighted ones), and a
|
||||
// String / DateTime node's numeric ones are refused by the server's shared HistoryAggregateDataTypes rule before this.
|
||||
public bool SupportsAggregateFor(HistoryAggregateType aggregate, TypeCode? valueType) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
// Data + events; every sample row stores ServerTimeUtc (written by the recorder / HistoryUpdate) and a raw read
|
||||
// returns it (lmxopcua#881).
|
||||
|
||||
Reference in New Issue
Block a user