M3 probe: non-streamed write transaction reachable over 2023 R2 gRPC (Begin/End live-verified)

The D2 storage-engine-pipe wall is WCF-transport-specific. On the 2023 R2 gRPC
front door, TransactionService is a first-class service AND the gateway to the
storage engine, so the Open2 storage-session GUID (uppercase) is accepted
directly as strHandle with no legacy pipe.

Live-verified against the real 2023 R2 server over a write-enabled (0x401) gRPC
session: AddNonStreamValuesBegin returns a real strTransactionId, and
AddNonStreamValuesEnd(bCommit=false) discards it cleanly (no data written). On
2020 WCF the same op returns UnknownClient(51) for every handle + priming chain.

- HistorianGrpcRevisionProbe + grpc-revision-probe CLI command + gated test
  NonStreamedWriteTransaction_OverGrpc_BeginsAndDiscards (live pass).
- HistorianGrpcHandshake.OpenSession gains an optional connectionMode param
  (default read-only 0x402; pass 0x401 for write-enabled) — non-breaking.
- Docs: revision-write-path.md "the wall is gone" section; roadmap M3 section,
  R3.1-R3.3 rows, one-glance table, and status note updated honestly.

Not yet shipped: the AddNonStreamValues btInput VTQ buffer is uncaptured (never
guess wire bytes), so no value-commit is implemented. Scope is non-streamed
ORIGINAL backfill; revision EDITS (R4.2) remain pipe-only even on gRPC.

272 unit tests pass; sanitization scan clean.

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 17:51:17 -04:00
parent 04ea0b9a1f
commit 23798db1ef
6 changed files with 319 additions and 17 deletions
@@ -1,3 +1,4 @@
using AVEVA.Historian.Client.Grpc;
using AVEVA.Historian.Client.Models;
namespace AVEVA.Historian.Client.Tests;
@@ -123,6 +124,30 @@ public sealed class HistorianGrpcIntegrationTests
Assert.All(names, n => Assert.StartsWith("Sys", n, StringComparison.Ordinal));
}
[Fact]
public async Task NonStreamedWriteTransaction_OverGrpc_BeginsAndDiscards()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
{
return;
}
// M3 reachability probe: on 2020 WCF this op group is walled (TransactionService relay
// returns UnknownClient(51) — the storage-engine-pipe requirement, see
// docs/plans/revision-write-path.md). On the 2023 R2 gRPC front door the native client
// passes the Open2 storage-session GUID straight to TransactionService and it works.
// This asserts the wall is gone: a write-enabled session opens and AddNonStreamValuesBegin
// returns a transaction id, which we immediately End with bCommit=false (writes nothing).
var probe = new HistorianGrpcRevisionProbe(BuildOptions(host));
HistorianGrpcRevisionProbeResult result = await probe.ProbeBeginAsync(CancellationToken.None);
Assert.True(result.OpenSucceeded);
Assert.True(result.BeginSucceeded, "AddNonStreamValuesBegin should return a transaction id over gRPC.");
Assert.False(string.IsNullOrEmpty(result.BeginTransactionId));
Assert.True(result.EndDiscardSucceeded, "AddNonStreamValuesEnd(bCommit:false) should discard cleanly.");
}
private static HistorianClientOptions BuildOptions(string host)
{
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");