Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayReadLimits.cs
T

85 lines
4.4 KiB
C#

using System.Globalization;
using System.Text.RegularExpressions;
using Grpc.Core;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
/// <summary>
/// The HistorianGateway's raw-read row ceiling (<c>Historian:ReadLimits:MaxValuesCeiling</c>, default 1,000,000) as seen
/// from this side of the wire. With <c>AllowStreamingBeyondCeiling=false</c> (a "strict" gateway) the gateway refuses
/// any <c>ReadRaw</c> whose <c>max_values</c> exceeds the ceiling with <c>InvalidArgument</c> — BEFORE it streams a row
/// — and names the ceiling in the refusal (<c>"max_values exceeds the server ceiling of {n}; …"</c>). The gateway has
/// no RPC that reports the ceiling, so the refusal is how it is learned (#691.3).
/// </summary>
/// <remarks>
/// A read whose budget the gateway refuses is re-issued ONCE with the ceiling as its budget
/// (<see cref="ReadWithCeilingRetryAsync{T}"/>); the caller then treats a stream that delivered the whole ceiling as
/// possibly truncated (never answered from). Nothing is cached: a re-configured gateway is honoured on the next read,
/// and a refusal costs one round-trip with no historian work behind it (it is validated before a session is acquired).
/// </remarks>
internal static class GatewayReadLimits
{
private const string CeilingRejectionText = "max_values exceeds the server ceiling";
private static readonly Regex CeilingNumber = new(@"max_values exceeds the server ceiling of (\d+)",
RegexOptions.CultureInvariant | RegexOptions.Compiled);
/// <summary>
/// True when <paramref name="ex"/> (or an inner exception — the client package wraps the <see cref="RpcException"/>)
/// is the gateway's <c>InvalidArgument</c> "max_values exceeds the server ceiling" rejection.
/// </summary>
/// <param name="ex">The exception.</param>
/// <returns>Whether it is the ceiling rejection.</returns>
public static bool IsCeilingRejection(Exception ex) => Rejection(ex) is not null;
/// <summary>
/// The ceiling a ceiling rejection names, or — when <paramref name="ex"/> is a ceiling rejection that does not
/// carry a parseable number — the gateway's default (<see cref="RawAggregateReader.GatewayDefaultMaxValuesCeiling"/>).
/// </summary>
/// <param name="ex">The exception.</param>
/// <param name="ceiling">The ceiling (&gt; 0) when this returns true.</param>
/// <returns>Whether <paramref name="ex"/> is a ceiling rejection.</returns>
public static bool TryGetCeiling(Exception ex, out int ceiling)
{
ceiling = 0;
if (Rejection(ex) is not { } rpc) return false;
var m = CeilingNumber.Match(rpc.Status.Detail);
ceiling = m.Success && int.TryParse(m.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out var n) && n > 0
? n
: RawAggregateReader.GatewayDefaultMaxValuesCeiling;
return true;
}
/// <summary>
/// Runs <paramref name="pass"/> with <paramref name="budget"/>; if a strict gateway refuses that budget, runs it once
/// more with the ceiling it named — only when that ceiling is smaller than the refused budget (otherwise the refusal
/// is rethrown). The refusal arrives before any row, so the first pass has consumed nothing.
/// </summary>
/// <typeparam name="T">The pass result.</typeparam>
/// <param name="budget">The <c>max_values</c> to ask for first.</param>
/// <param name="pass">One drained read with the given budget.</param>
/// <returns>The pass result and the budget it actually used.</returns>
public static async Task<(T Result, int Budget)> ReadWithCeilingRetryAsync<T>(int budget, Func<int, Task<T>> pass)
{
int ceiling;
try
{
return (await pass(budget).ConfigureAwait(false), budget);
}
catch (Exception ex) when (TryGetCeiling(ex, out ceiling) && ceiling < budget)
{
// fall through to the retry (outside the catch, so a second refusal is not nested in this one)
}
return (await pass(ceiling).ConfigureAwait(false), ceiling);
}
private static RpcException? Rejection(Exception ex)
{
for (var e = ex; e is not null; e = e.InnerException)
if (e is RpcException { StatusCode: StatusCode.InvalidArgument } rpc
&& rpc.Status.Detail.Contains(CeilingRejectionText, StringComparison.Ordinal))
return rpc;
return null;
}
}