fix(historian): gateway AGGREGATE_NOT_SUPPORTED_FOR_TAG -> BadAggregateNotSupported, no health failure (#900)
v2-ci / unit-tests (push) Has been cancelled

A node declared numeric whose AVEVA tag is discrete (or string) is not refused up front by #897, so its native
TimeAverage / Total still reach AVEVA, which refuses them. A HistorianGateway at or after historiangw#135
(a220ec0) answers that FailedPrecondition with a google.rpc.ErrorInfo (reason AGGREGATE_NOT_SUPPORTED_FOR_TAG,
domain historian_gateway.v1) in grpc-status-details-bin. The Gateway backend's native processed read now maps
exactly that to HistoryAggregateNotSupportedException (BadAggregateNotSupported for the node) without recording
a health failure or treating it as transport. Any other FailedPrecondition and every Internal (an older gateway's
answer for the same case) keep today's handling.

Detection lives in one place, GatewayErrorReasons. The HistorianGateway.Client pin stays 1.0.0 (1.1.0 is not on
the feed): 1.0.0 keeps the gateway's RpcException, trailers included, as the InnerException of the
HistorianGatewayException it throws, so the reason is read with Grpc.StatusProto 2.76.0 (added via central
package management; nuget.org; same version as Grpc.Core.Api; depends on Google.Api.CommonProtos 2.16.0, which
accepts Google.Protobuf 3.34.1). With Client 1.1.0, ReasonOf becomes
`(ex as HistorianGatewayException)?.ErrorReason` and the StatusProto reference can go.

Tests:
- GatewayAggregateNotSupportedWireTests: in-process gRPC gateway sending the real status + ErrorInfo shape
  (copied from historiangw HistorianRpcFaults.ToRpcException), read through the PUBLISHED Client 1.0.0 via the
  production adapter: TimeAverage / Total -> HistoryAggregateNotSupportedException, 0 health failures; controls
  (Internal; FailedPrecondition without ErrorInfo; other reason; other domain; the reason on Internal) are
  rethrown, not IOException, and counted; positive control served.
- DiscreteTagTimeWeightedWireTests: over a real SDK session, a Double / Int32 node on the discrete tag ->
  BadAggregateNotSupported with 0 failures; the older gateway's Internal answer keeps its status and counts.
- AvevaGatewayAggregateRefusalLiveGateTests: opt-in read-only live gate (OTOPCUA_AVEVA_GATE_900=true).
Mutations (catch removed; status ignored; domain ignored; reason ignored; inner-exception walk removed) each
fail at least one test. Release build 0 warnings; Historian.Gateway.Tests 601/607 (6 skipped), OpcUaServer
IntegrationTests 323/361 (38 skipped).

Live, read-only: private HistorianGateway container from a220ec0 (API-key auth disabled, live writes off,
removed after) against wonder-sql-vd03: Double and Int32 nodes on SysPulse, TimeAverage and Total ->
BadAggregateNotSupported, health failures unchanged (0); SysTimeSec TimeAverage Good (2 rows); SysPulse Average
served. Gateway traffic was ReadAggregate / ReadRaw / GetTagMetadata only.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
Joseph Doherty
2026-11-13 01:31:52 -05:00
parent 6c4d29902e
commit efaf3435ea
8 changed files with 495 additions and 6 deletions
+8 -3
View File
@@ -1592,9 +1592,14 @@ serve a Boolean node's numeric aggregates as 0/1 (stepped for the time-weighted
true), and the Gateway's raw-evaluated Average/Min/Max/… do the same. So on a Boolean node TimeAverage / Total are served by
`SqlServer` / `QuestDb` and refused by `Gateway`. An unknown node type (BaseDataType, an array) is not refused.
The residual case — a node declared numeric whose AVEVA tag is discrete — still reaches AVEVA and answers
`BadHistoryOperationUnsupported`, counted as a failure, until the gateway surfaces AVEVA's native error code as a
deterministic status.
The residual case — a node declared numeric whose AVEVA tag is discrete (or string) — still reaches AVEVA. A gateway at or
after historiangw#135 (`a220ec0`) answers it `FailedPrecondition` with a `google.rpc.ErrorInfo` (reason
`AGGREGATE_NOT_SUPPORTED_FOR_TAG`, domain `historian_gateway.v1`) in `grpc-status-details-bin`; the Gateway backend maps that
to **`BadAggregateNotSupported`** for the node and counts **no** backend failure (#900, live-verified read-only against
wonder-sql-vd03 `SysPulse`). Only that reason is matched: any other `FailedPrecondition` and every `Internal` keep the old
handling, so an older gateway (which answers this case `Internal`) still gives `BadHistoryOperationUnsupported`, counted as a
failure. The reason is read in one place, `GatewayErrorReasons` — from the trailer via `Grpc.StatusProto` while the
HistorianGateway Client pin is 1.0.0; with Client 1.1.0 it becomes `HistorianGatewayException.ErrorReason`.
⚠️ **`Server/ServerCapabilities/AggregateFunctions` (`i=2997`) is deliberately NOT trimmed** — it keeps the SDK's full 37 in
every configuration, including with no historian. Part 5 defines it as every aggregate the *server* supports, and the server