R4.3: measured idle-state GetStoreForwardStatusAsync over gRPC
Route GetStoreForwardStatusAsync to a gRPC path that actually contacts the server (StatusService.GetHistorianConsoleStatus) instead of synthesizing an all-false result blind. On a reachable/normal server it returns the not-storing baseline but MEASURED; when the server is unreachable or the console-status call fails it reports ErrorOccurred with the underlying error (the old synthesis never contacted the server). The active-SF buffer magnitude (Storing/Pending/DataStored) stays false because it lives behind the D2 storage-engine console wall. Non-gRPC transports keep the synthesized fallback. Live-verified against the 2023 R2 server; gated integration test GetStoreForwardStatusAsync_OverGrpc_ReturnsMeasuredIdleState added. README operation table updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Grpc.Core;
|
||||
using AVEVA.Historian.Client.Models;
|
||||
using GrpcStatus = ArchestrA.Grpc.Contract.Status;
|
||||
|
||||
namespace AVEVA.Historian.Client.Grpc;
|
||||
@@ -36,6 +37,78 @@ internal static class HistorianGrpcStatusClient
|
||||
return (response.Status?.BSuccess ?? false) ? response.StrParameterValue : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <em>measured</em> store-forward status over the 2023 R2 gRPC transport (R4.3).
|
||||
/// <para>
|
||||
/// Unlike the non-gRPC <see cref="Wcf.HistorianWcfStatusClient"/> path — which synthesizes an
|
||||
/// all-false result <em>without contacting the server</em> — this opens an authenticated session
|
||||
/// and calls <c>StatusService.GetHistorianConsoleStatus</c>, the only SF-adjacent signal reachable
|
||||
/// from a pure managed client. (The direct <c>StorageService.GetSFParameter</c> /
|
||||
/// <c>GetRemainingSnapshotsSize</c> RPCs that carry the SF buffer magnitude require the
|
||||
/// <c>OpenStorageConnection</c> storage-engine console handle, which is gated behind the D2
|
||||
/// storage-engine-pipe wall and is unobtainable here — see
|
||||
/// <c>docs/plans/store-forward-cache-reverse-engineering.md</c> §9.7.)
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Semantics: a successful console-status read means the server is reachable and its storage
|
||||
/// console is reporting normally ⇒ the not-storing baseline (all flags false), but now
|
||||
/// <em>measured</em> rather than blindly assumed. If the server cannot be reached/authenticated,
|
||||
/// or the console-status call itself fails, <see cref="HistorianStoreForwardStatus.ErrorOccurred"/>
|
||||
/// is set with the underlying error. The active-SF state (<see cref="HistorianStoreForwardStatus.Storing"/>
|
||||
/// / <see cref="HistorianStoreForwardStatus.Pending"/> / <see cref="HistorianStoreForwardStatus.DataStored"/>
|
||||
/// magnitude) is NOT observable from this signal and remains false; populating it requires the
|
||||
/// D2-gated storage-console path.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static Task<HistorianStoreForwardStatus> GetStoreForwardStatusAsync(
|
||||
HistorianClientOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
=> Task.Run(() => GetStoreForwardStatus(options, cancellationToken), cancellationToken);
|
||||
|
||||
private static HistorianStoreForwardStatus GetStoreForwardStatus(HistorianClientOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
HistorianStoreForwardStatus NotStoring(bool errorOccurred, string? error) => new(
|
||||
ServerName: options.Host,
|
||||
Pending: false,
|
||||
ErrorOccurred: errorOccurred,
|
||||
Error: error,
|
||||
DataStored: false,
|
||||
Storing: false,
|
||||
ConnectionKind: HistorianConnectionKind.Process);
|
||||
|
||||
try
|
||||
{
|
||||
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
|
||||
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken);
|
||||
|
||||
var statusClient = new GrpcStatus.StatusService.StatusServiceClient(connection.Channel);
|
||||
GrpcStatus.GetHistorianConsoleStatusResponse response = statusClient.GetHistorianConsoleStatus(
|
||||
new GrpcStatus.GetHistorianConsoleStatusRequest { StrHandle = session.StringHandle },
|
||||
connection.Metadata,
|
||||
DateTime.UtcNow.Add(options.RequestTimeout),
|
||||
cancellationToken);
|
||||
|
||||
if (response.Status?.BSuccess ?? false)
|
||||
{
|
||||
// Measured: server reachable, storage console reporting normally → not-storing baseline.
|
||||
return NotStoring(errorOccurred: false, error: null);
|
||||
}
|
||||
|
||||
byte[] err = response.Status?.BtError?.ToByteArray() ?? [];
|
||||
string detail = err.Length == 0 ? "GetHistorianConsoleStatus returned failure." : Convert.ToHexString(err);
|
||||
return NotStoring(errorOccurred: true, error: $"GetHistorianConsoleStatus failed: {detail}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Server unreachable / auth failed — genuinely measured: report it instead of a silent all-false.
|
||||
return NotStoring(errorOccurred: true, error: $"{ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the Historian server's system time-zone name (roadmap item R1.3,
|
||||
/// <c>StatusService.GetSystemTimeZoneName</c>). Unlike the 2020 WCF surface — where the native
|
||||
|
||||
@@ -57,7 +57,14 @@ internal sealed class Historian2020ProtocolDialect
|
||||
public Task<HistorianStoreForwardStatus> GetStoreForwardStatusAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Wcf.HistorianWcfStatusClient.GetStoreForwardStatusAsync(_options, cancellationToken);
|
||||
|
||||
// Over gRPC (2023 R2) we return a MEASURED idle-state: the client actually contacts the server
|
||||
// (GetHistorianConsoleStatus) and reports ErrorOccurred when unreachable. The active-SF buffer
|
||||
// magnitude lives behind the D2 storage-engine console wall and stays false. Non-gRPC transports
|
||||
// keep the synthesized all-false (no SF sidecar to probe). See R4.3 §9.7.
|
||||
return UseGrpc
|
||||
? HistorianGrpcStatusClient.GetStoreForwardStatusAsync(_options, cancellationToken)
|
||||
: Wcf.HistorianWcfStatusClient.GetStoreForwardStatusAsync(_options, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string?> GetSystemParameterAsync(string name, CancellationToken cancellationToken)
|
||||
|
||||
Reference in New Issue
Block a user