docs(grpc): reflect newly-tooled config ops in the transport matrix

- GetRuntimeParameter / GetTagExtendedProperties now live-verified over gRPC
- ExecuteSqlCommand marked server-walled (new legend state)
- tag-config writes marked sandbox-gated (new legend state)
- document the HISTORIAN_GRPC_WRITE_SANDBOX_TAG live-test gate
- rewrite the matrix summary to reflect what was learned tooling the config ops

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-22 01:26:34 -04:00
parent 0780cec9a7
commit e7a6cf1989
+26 -17
View File
@@ -66,8 +66,11 @@ Open2 buffers and SSPI tokens on both — on gRPC they simply ride inside protob
`bytes` fields — so reads are at parity. The surfaces diverge at the edges.
Legend: ✅ tooled + live-verified · ⚠️ tooled, partial/synthesized ·
🔌 **the gRPC server exposes the RPC (recovered in `Grpc/Protos/*.proto`) but the
SDK doesn't drive it yet** — untooled/uncaptured, *not* a protocol gap ·
🧪 tooled + routed but **sandbox-gated** (mutates server state, not yet run
destructively against a live box) · 🔌 **the gRPC server exposes the RPC
(recovered in `Grpc/Protos/*.proto`) but the SDK doesn't drive it yet** —
untooled/uncaptured, *not* a protocol gap · ⛔ tooled but **server-walled** (the
request rides the RPC but the server faults on an unmet precondition) ·
❌ unavailable on that transport.
| Operation | WCF | gRPC | Notes |
@@ -82,27 +85,32 @@ SDK doesn't drive it yet** — untooled/uncaptured, *not* a protocol gap ·
| `AddHistoricalValuesAsync` | ❌ | ✅ | historical/backfill writes ride `HistoryService.AddStreamValues`; non-gRPC throws `ProtocolEvidenceMissingException` |
| `GetServerTimeZoneAsync` | ❌ | ✅ | 2020 `GetSystemTimeZoneName` is a client-side stub (empty); WCF throws |
| `GetStoreForwardStatusAsync` | ⚠️ | ✅ | gRPC contacts the server (measured idle-state, reports `ErrorOccurred`); WCF returns synthesized all-false. Active-SF magnitude is D2-gated on both |
| `ReadEventsAsync` | ✅ | 🔌 | gRPC `RetrievalService.StartEventQuery` / `GetNextEventQueryResultBuffer` / `EndEventQuery` recovered (`bytes btRequest` + handle); not tooled over gRPC |
| `GetRuntimeParameterAsync` | ✅ | | tooled + live-verified over gRPC (`StatusService.GetRuntimeParameter`, the 2020 `GETRP` buffers ride unchanged) |
| `GetTagExtendedPropertiesAsync` | ✅ | ✅ | tooled + live-verified over gRPC (`RetrievalService.GetTagExtendedPropertiesFromName`, the `GetTepByNm` buffers ride unchanged) |
| `ExecuteSqlCommandAsync` | ✅ | ⛔ | gRPC request rides `RetrievalService.ExecuteSqlCommand`, but the server-side `CSrvDbConnection.ExecuteSqlCommand` faults (`IndexOutOfRange`, native err 38) — an unmet DB-connection precondition; bounded behind `ProtocolEvidenceMissingException`. Use WCF |
| `ReadEventsAsync` | ✅ | 🔌 | gRPC `StartEventQuery`/`GetNextEventQueryResultBuffer`/`EndEventQuery` recovered, but the read needs the full CM_EVENT registration state machine (RTag2+EnsT2) ported — not yet tooled |
| `SendEventAsync` | ✅ | 🔌 | rides `AddStreamValues` family; no distinct event-send RPC, framing uncaptured over gRPC |
| `EnsureTagAsync` / `DeleteTagAsync` / `RenameTagsAsync` | ✅ | 🔌 | gRPC `HistoryService.EnsureTags` / `DeleteTags` / `StartJob`(+`GetJobStatus`) recovered (`bytes btTagInfos`/`btTagnames`/`btInput` + handle) |
| `GetTagExtendedPropertiesAsync` / `AddTagExtendedPropertiesAsync` | ✅ | 🔌 | gRPC `RetrievalService.GetTagExtendedPropertiesFromName` + `HistoryService.AddTagExtendedProperties`; gRPC also exposes `DeleteTagExtendedProperties` (WCF delete was server-blocked) |
| `ExecuteSqlCommandAsync` | ✅ | 🔌 | gRPC `RetrievalService.ExecuteSqlCommand` (`StrCommand` + `uiOption`, mirrors WCF `ExeC`/`GetR`) |
| `GetRuntimeParameterAsync` | ✅ | 🔌 | gRPC `StatusService.GetRuntimeParameter` (`bytes btRequest` + handle) |
| `EnsureTagAsync` / `DeleteTagAsync` / `RenameTagsAsync` | ✅ | 🧪 | tooled + routed over gRPC (`HistoryService.EnsureTags` / `DeleteTags` / `StartJob`, write-enabled 0x401 session, WCF serializers reused); sandbox-gated — not yet run destructively against a live box |
| `AddTagExtendedPropertiesAsync` | ✅ | 🧪 | tooled + routed over gRPC (`HistoryService.AddTagExtendedProperties`, write-enabled session); sandbox-gated. gRPC also exposes `DeleteTagExtendedProperties` (WCF delete was server-blocked) |
| `GetConnectionStatusAsync` | ✅ | ❌ | synthesized from an authenticated probe — no dedicated RPC on either transport (gRPC `PingServer`/`GetHistorianConsoleStatus` could synthesize it) |
| `ReadBlocksAsync` | ❌ | ❌ | `StartBlockRetrievalQuery` never captured on either transport — throws `ProtocolEvidenceMissingException` |
In short: **WCF is the broad, mature surface** (every config write, events, SQL,
and all reads), while **gRPC is the narrower *tooled* surface** — but the 2023 R2
gRPC *contract* is actually a **superset** of WCF. Every 🔌 row above has a
recovered RPC carrying the **same opaque `bytes` buffers the existing WCF
serializers already emit**, keyed by the same `strHandle`/`uiHandle` session
handle the gRPC read path already obtains. So these are **capture-and-wire** items
(route the existing serializer into a gRPC orchestrator + golden-capture the
framing), **not** protocol-discovery items. We have only *buffer-verified* two
gRPC families live — the read chain and `AddStreamValues` — so per the
"capture first, never guess wire bytes" rule the 🔌 rows stay untooled until each
is captured. The natural production pattern today remains WCF for config/reads and
`RemoteGrpc` reserved for `AddHistoricalValuesAsync`.
gRPC *contract* is actually a **superset** of WCF. The recovered config RPCs carry
the **same opaque `bytes` buffers the existing WCF serializers already emit**,
keyed by the same `strHandle`/`uiHandle` session handle the read path obtains —
confirmed by tooling the read-side config ops (`GetRuntimeParameter`,
`GetTagExtendedProperties`) live: the WCF buffers ride the gRPC RPC unchanged and
the server accepts them. Two caveats surfaced when capturing the rest: `ExecuteSqlCommand`
is **server-walled** (the front-door `CSrvDbConnection` faults on a DB-connection
precondition the managed session doesn't establish — the same *class* of wall as
`OpenStorageConnection`), and `ReadEvents` needs the CM_EVENT registration state
machine ported. The remaining 🔌 rows are **capture-and-wire** items (route the
existing serializer into a gRPC orchestrator + live-capture), not
protocol-discovery — but per "capture first, never guess wire bytes" they stay
untooled until each is verified live. The natural production pattern today remains
WCF for config/writes and `RemoteGrpc` for reads + `AddHistoricalValuesAsync`.
> A 2023 R2 server reports History interface version 12 (vs. 11 on 2020). The
> connect-time version gate accepts both — they are byte-compatible — so gRPC
@@ -216,6 +224,7 @@ $env:HISTORIAN_GRPC_TLS = 'true' # gRPC over TLS
$env:HISTORIAN_GRPC_DNSID = 'my-2023r2-host' # cert DNS name when connecting by IP
$env:HISTORIAN_GRPC_TIMEOUT = '120' # per-call deadline (s); raise for slow links
$env:HISTORIAN_WRITE_SANDBOX_TAG = 'MyFloatTag' # gates the AddHistoricalValues write test
$env:HISTORIAN_GRPC_WRITE_SANDBOX_TAG = 'SandboxTag' # gates the DESTRUCTIVE tag create/rename/delete lifecycle test
```
The aggregate tests self-calibrate their query window from a real raw sample, so