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:
Joseph Doherty
2026-06-21 23:14:17 -04:00
parent c2d8fb9bc8
commit 53a9c87114
4 changed files with 107 additions and 2 deletions
@@ -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