chore(historian): HistorianGateway Client/Contracts 1.1.0, typed error reason (#900)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
Bump ZB.MOM.WW.HistorianGateway.Client + Contracts 1.0.0 -> 1.1.0 (historiangw#135, release 6365f34). GatewayErrorReasons now reads Client 1.1.0's typed HistorianGatewayException.StatusCode / ErrorReason / ErrorDomain (constants from HistorianGatewayErrorReasons) instead of parsing grpc-status-details-bin itself; the direct Grpc.StatusProto pin and reference are dropped (the client brings it). The published client maps every RpcException to a HistorianGatewayException, so the trailer fallback is not needed. Behaviour unchanged: FailedPrecondition + AGGREGATE_NOT_SUPPORTED_FOR_TAG (domain historian_gateway.v1) -> BadAggregateNotSupported, everything else keeps its handling. Adds GatewayErrorReasonsTests (typed-property-only exceptions, so they fail against the old trailer implementation); wire tests and docs/Historian.md updated. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -1,72 +1,48 @@
|
||||
using Grpc.Core;
|
||||
using ZB.MOM.WW.HistorianGateway.Client;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
|
||||
|
||||
/// <summary>
|
||||
/// The HistorianGateway's machine-readable failure reasons (historiangw#135): a <c>google.rpc.ErrorInfo</c> the gateway
|
||||
/// attaches in the standard <c>grpc-status-details-bin</c> trailer, domain <see cref="Domain"/>. The ONE place OtOpcUa
|
||||
/// reads them (lmxopcua#900).
|
||||
/// attaches in the standard <c>grpc-status-details-bin</c> trailer, domain <see cref="HistorianGatewayErrorReasons.Domain"/>.
|
||||
/// The ONE place OtOpcUa reads them (lmxopcua#900).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Client pin is 1.0.0, which does not surface the reason, but keeps the gateway's <see cref="RpcException"/> —
|
||||
/// trailers included — as the <see cref="Exception.InnerException"/> of the <c>HistorianGatewayException</c> it throws, so
|
||||
/// <see cref="ReasonOf"/> parses the trailer with <c>Grpc.StatusProto</c>. When the pin moves to Client 1.1.0 (not yet on the
|
||||
/// feed), <see cref="ReasonOf"/> becomes the one line
|
||||
/// <c>(ex as ZB.MOM.WW.HistorianGateway.Client.HistorianGatewayException)?.ErrorReason</c> (and the constants can point
|
||||
/// at <c>HistorianGatewayErrorReasons</c>); the <c>Grpc.StatusProto</c> reference can then go.
|
||||
/// Client 1.1.0 parses the trailer itself and exposes it on every <see cref="HistorianGatewayException"/> it throws
|
||||
/// (<see cref="HistorianGatewayException.StatusCode"/>, <see cref="HistorianGatewayException.ErrorReason"/>,
|
||||
/// <see cref="HistorianGatewayException.ErrorDomain"/>), so this reads those typed properties. The 1.0.0-era trailer
|
||||
/// parsing (<c>Grpc.StatusProto</c> over the inner <see cref="RpcException"/>) is gone: the published client maps every
|
||||
/// <see cref="RpcException"/> of every call to a <see cref="HistorianGatewayException"/> (or an
|
||||
/// <see cref="OperationCanceledException"/> for a caller cancellation), so no raw one reaches the data source.
|
||||
/// </remarks>
|
||||
internal static class GatewayErrorReasons
|
||||
{
|
||||
/// <summary>The <c>ErrorInfo.domain</c> of every reason the gateway sends (its proto package).</summary>
|
||||
public const string Domain = "historian_gateway.v1";
|
||||
|
||||
/// <summary>
|
||||
/// AVEVA refused the aggregate retrieval mode for this tag (TimeWeightedAverage / Integral / … on a discrete or string
|
||||
/// tag; native CustomError 85 at StartQuery). Sent with <see cref="StatusCode.FailedPrecondition"/>.
|
||||
/// </summary>
|
||||
public const string AggregateNotSupportedForTag = "AGGREGATE_NOT_SUPPORTED_FOR_TAG";
|
||||
|
||||
/// <summary>
|
||||
/// The gateway's <c>ErrorInfo.reason</c> on <paramref name="ex"/> (or an inner exception — the client package wraps the
|
||||
/// <see cref="RpcException"/>), or <see langword="null"/> when it sent none, sent one from another domain, or sent an
|
||||
/// unparseable trailer.
|
||||
/// The gateway's <c>ErrorInfo.reason</c> on <paramref name="ex"/> (or an inner exception), or <see langword="null"/>
|
||||
/// when it sent none or sent one from another domain.
|
||||
/// </summary>
|
||||
/// <param name="ex">A gateway call's exception.</param>
|
||||
/// <returns>The reason string, or <see langword="null"/>.</returns>
|
||||
public static string? ReasonOf(Exception ex)
|
||||
{
|
||||
for (var e = ex; e is not null; e = e.InnerException)
|
||||
{
|
||||
if (e is not RpcException rpc) continue;
|
||||
try
|
||||
{
|
||||
var info = rpc.GetRpcStatus()?.GetDetail<Google.Rpc.ErrorInfo>();
|
||||
return info is not null && string.Equals(info.Domain, Domain, StringComparison.Ordinal) ? info.Reason : null;
|
||||
}
|
||||
catch (Google.Protobuf.InvalidProtocolBufferException)
|
||||
{
|
||||
return null; // a malformed trailer is no reason: the failure keeps its ordinary handling
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public static string? ReasonOf(Exception ex) =>
|
||||
GatewayExceptionOf(ex) is { ErrorDomain: HistorianGatewayErrorReasons.Domain } gw ? gw.ErrorReason : null;
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="ex"/> is the gateway's refusal of an aggregate for the tag: status
|
||||
/// <see cref="StatusCode.FailedPrecondition"/> AND reason <see cref="AggregateNotSupportedForTag"/>. FailedPrecondition alone
|
||||
/// is not enough — the gateway also uses it for disabled live writes and passes historian-side ones through — and an
|
||||
/// older gateway answers this case <c>Internal</c> with no reason (unchanged handling).
|
||||
/// <see cref="StatusCode.FailedPrecondition"/> AND reason <see cref="HistorianGatewayErrorReasons.AggregateNotSupportedForTag"/>.
|
||||
/// FailedPrecondition alone is not enough — the gateway also uses it for disabled live writes and passes historian-side
|
||||
/// ones through — and an older gateway answers this case <c>Internal</c> with no reason (unchanged handling).
|
||||
/// </summary>
|
||||
/// <param name="ex">A gateway call's exception.</param>
|
||||
/// <returns>Whether it is the aggregate refusal.</returns>
|
||||
public static bool IsAggregateNotSupportedForTag(Exception ex) =>
|
||||
StatusOf(ex) == StatusCode.FailedPrecondition
|
||||
&& string.Equals(ReasonOf(ex), AggregateNotSupportedForTag, StringComparison.Ordinal);
|
||||
GatewayExceptionOf(ex)?.StatusCode == StatusCode.FailedPrecondition
|
||||
&& string.Equals(ReasonOf(ex), HistorianGatewayErrorReasons.AggregateNotSupportedForTag, StringComparison.Ordinal);
|
||||
|
||||
private static StatusCode? StatusOf(Exception ex)
|
||||
private static HistorianGatewayException? GatewayExceptionOf(Exception ex)
|
||||
{
|
||||
for (var e = ex; e is not null; e = e.InnerException)
|
||||
if (e is RpcException rpc) return rpc.StatusCode;
|
||||
if (e is HistorianGatewayException gw) return gw;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -25,8 +25,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ZB.MOM.WW.HistorianGateway.Client" />
|
||||
<PackageReference Include="ZB.MOM.WW.HistorianGateway.Contracts" />
|
||||
<!-- lmxopcua#900: GatewayErrorReasons reads the gateway's ErrorInfo trailer (until the Client 1.1.0 pin). -->
|
||||
<PackageReference Include="Grpc.StatusProto" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<!-- FasterLog-backed durable historization outbox (Recorder/FasterLogHistorizationOutbox).
|
||||
Pure-managed FasterLog; same package the gateway's store-forward outbox uses. -->
|
||||
|
||||
Reference in New Issue
Block a user