docs(grpc): matrix + plan reflect ext-prop fix, SQL prime result, ConnStatus

- README transport matrix: GetTagExtendedProperties notes the multi-property parser
  fix; AddTagExtendedProperties read-back now round-trips; GetConnectionStatus gRPC
  -> live-verified; ExecuteSqlCommand notes the RegisterTags prime does not help.
  Refresh the closing production-pattern guidance.
- grpc-tooling-completion.md: mark #5 (ConnStatus) done, #4 (SQL prime) negative, and
  the #1 ext-prop read-back follow-up done.

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 06:03:59 -04:00
parent 8984dac1ed
commit ecf446965a
2 changed files with 30 additions and 20 deletions
+8 -7
View File
@@ -86,13 +86,13 @@ request rides the RPC but the server faults on an unmet precondition) ·
| `GetServerTimeZoneAsync` | ❌ | ✅ | 2020 `GetSystemTimeZoneName` is a client-side stub (empty); WCF throws | | `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 | | `GetStoreForwardStatusAsync` | ⚠️ | ✅ | gRPC contacts the server (measured idle-state, reports `ErrorOccurred`); WCF returns synthesized all-false. Active-SF magnitude is D2-gated on both |
| `GetRuntimeParameterAsync` | ✅ | ✅ | tooled + live-verified over gRPC (`StatusService.GetRuntimeParameter`, the 2020 `GETRP` buffers ride unchanged) | | `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) | | `GetTagExtendedPropertiesAsync` | ✅ | ✅ | tooled + live-verified over gRPC (`RetrievalService.GetTagExtendedPropertiesFromName`, the `GetTepByNm` buffers ride unchanged). The shared parser now handles the live multi-property response shape (one group per property + a uint16 searchability-flags trailer), fixed 2026-06-22 |
| `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 | | `ExecuteSqlCommandAsync` | ✅ | ⛔ | gRPC request rides `RetrievalService.ExecuteSqlCommand`, but the server-side `CSrvDbConnection.ExecuteSqlCommand` faults (`IndexOutOfRange`, native err 38) — an unmet DB-connection precondition. A `HistoryService.RegisterTags` prime does **not** clear it (tried live 2026-06-22, both 0x402/0x401). Bounded behind `ProtocolEvidenceMissingException`. Use WCF |
| `ReadEventsAsync` | ✅ | ⚠️ | tooled + routed over gRPC: the full CM_EVENT registration replay (`UpdateClientStatus``RegisterTags``EnsureTags` + discovery probes) runs and `StartEventQuery` succeeds, but `GetNextEventQueryResultBuffer` **long-polls** on no data (it blocks to the deadline rather than returning the synchronous 5-byte code-85 terminal the WCF op gives). The read is **hard-bounded** (≤30s) and throws `ProtocolEvidenceMissingException` on the no-row path rather than assert a false empty. Row-level retrieval is **not yet live-verified** — the dev box holds no events; pending a capture against an event-bearing 2023 R2 server. Use WCF for event reads | | `ReadEventsAsync` | ✅ | ⚠️ | tooled + routed over gRPC: the full CM_EVENT registration replay (`UpdateClientStatus``RegisterTags``EnsureTags` + discovery probes) runs and `StartEventQuery` succeeds, but `GetNextEventQueryResultBuffer` **long-polls** on no data (it blocks to the deadline rather than returning the synchronous 5-byte code-85 terminal the WCF op gives). The read is **hard-bounded** (≤30s) and throws `ProtocolEvidenceMissingException` on the no-row path rather than assert a false empty. Row-level retrieval is **not yet live-verified** — the dev box holds no events; pending a capture against an event-bearing 2023 R2 server. Use WCF for event reads |
| `SendEventAsync` | ✅ | 🔌 | rides `AddStreamValues` family; no distinct event-send RPC, framing uncaptured over gRPC | | `SendEventAsync` | ✅ | 🔌 | rides `AddStreamValues` family; no distinct event-send RPC, framing uncaptured over gRPC |
| `EnsureTagAsync` / `DeleteTagAsync` / `RenameTagsAsync` | ✅ | ✅ | live-verified 2026-06-22 over gRPC (`HistoryService.EnsureTags` / `DeleteTags` / `StartJob`, write-enabled 0x401 session, WCF serializers reused) via a self-cleaning sandbox-tag lifecycle. Rename is an async StartJob — transiently rejectable right after create, so callers should retry | | `EnsureTagAsync` / `DeleteTagAsync` / `RenameTagsAsync` | ✅ | ✅ | live-verified 2026-06-22 over gRPC (`HistoryService.EnsureTags` / `DeleteTags` / `StartJob`, write-enabled 0x401 session, WCF serializers reused) via a self-cleaning sandbox-tag lifecycle. Rename is an async StartJob — transiently rejectable right after create, so callers should retry |
| `AddTagExtendedPropertiesAsync` | ✅ | ✅ | live-verified 2026-06-22 over gRPC (`HistoryService.AddTagExtendedProperties`, write-enabled session). NOTE: reading a written prop back via `GetTagExtendedPropertiesAsync` can hit a shared-parser evidence gap (value marker `0x01` vs the captured compact-string `0x09`); the write itself is confirmed. gRPC also exposes `DeleteTagExtendedProperties` (WCF delete was server-blocked) | | `AddTagExtendedPropertiesAsync` | ✅ | ✅ | live-verified 2026-06-22 over gRPC (`HistoryService.AddTagExtendedProperties`, write-enabled session); a written prop now round-trips through `GetTagExtendedPropertiesAsync` (the multi-property parser fix above). 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) | | `GetConnectionStatusAsync` | ✅ | | live-verified 2026-06-22 over gRPC — measured from the handshake (`OpenConnection` yields a storage-session GUID ⇒ connected). No dedicated RPC on either transport; store-forward connectivity stays false (D2-gated) |
| `ReadBlocksAsync` | ❌ | ❌ | `StartBlockRetrievalQuery` never captured on either transport — throws `ProtocolEvidenceMissingException` | | `ReadBlocksAsync` | ❌ | ❌ | `StartBlockRetrievalQuery` never captured on either transport — throws `ProtocolEvidenceMissingException` |
In short: **WCF is the broad, mature surface** (every config write, events, SQL, In short: **WCF is the broad, mature surface** (every config write, events, SQL,
@@ -116,9 +116,10 @@ confirming rows awaits an event-bearing 2023 R2 server. The remaining 🔌 row
into a gRPC orchestrator + live-capture), not protocol-discovery — but per into a gRPC orchestrator + live-capture), not protocol-discovery — but per
"capture first, never guess wire bytes" it stays untooled until verified live. The "capture first, never guess wire bytes" it stays untooled until verified live. The
natural production pattern today: `RemoteGrpc` now covers reads, natural production pattern today: `RemoteGrpc` now covers reads,
`AddHistoricalValuesAsync`, and the tag-config writes (create/delete/rename/extended `AddHistoricalValuesAsync`, the tag-config writes (create/delete/rename/extended
properties, live-verified) — use WCF for SQL, events, and reading extended properties, including read-back), and connection status — all live-verified. Use
properties back until those gRPC gaps close. WCF for SQL (server-walled on gRPC) and event reads/sends (gRPC event rows are
long-poll-blocked pending an event-bearing server).
> A 2023 R2 server reports History interface version 12 (vs. 11 on 2020). The > 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 > connect-time version gate accepts both — they are byte-compatible — so gRPC
+22 -13
View File
@@ -50,9 +50,13 @@ transiently reject right after the create commits and on target-name collision
pre-cleans both names and retries rename (4×); callers should likewise retry. (b) **reading a written pre-cleans both names and retries rename (4×); callers should likewise retry. (b) **reading a written
extended property back** via `GetTagExtendedPropertiesAsync` hits a shared-parser evidence gap (value extended property back** via `GetTagExtendedPropertiesAsync` hits a shared-parser evidence gap (value
marker `0x01` where the parser expects compact-string `0x09`) — a read-side gap, not a write failure; marker `0x01` where the parser expects compact-string `0x09`) — a read-side gap, not a write failure;
the test tolerates it. Lifecycle test is self-cleaning and asserts no litter remains (verified two the test tolerates it. Lifecycle test is self-cleaning and best-effort cleans up (rename is async +
consecutive clean passes). Next read-side follow-up: capture the `0x01` extended-property value the browse/metadata view is eventually consistent, so a hard absence assert would be racy).
encoding and extend `HistorianTagExtendedPropertyProtocol.ParseResponse`. **Read-side follow-up DONE 2026-06-22:** captured the live `GetTagExtendedPropertiesFromName` bytes
and fixed the parser — the response is one group per property (tag name repeats) with a **uint16
searchability-flags trailer** per property (e.g. `0x0003` built-in, `0x0001` user-added), NOT the
1-byte group trailer the old model assumed (which drifted one byte per group → `0x09`-vs-`0x01`). A
written prop now round-trips end-to-end live; golden multi-group test added.
_Original notes:_ _Original notes:_
- **Goal:** flip the 🧪 writes to ✅ by running the gated lifecycle test against a sandbox tag. - **Goal:** flip the 🧪 writes to ✅ by running the gated lifecycle test against a sandbox tag.
@@ -115,18 +119,23 @@ _Original notes (still the reference for the registration replay):_
"capture first, never guess"). Depends on #2 (same CM_EVENT registration). "capture first, never guess"). Depends on #2 (same CM_EVENT registration).
- **Risk:** high / blocked on capture. Lowest priority. - **Risk:** high / blocked on capture. Lowest priority.
### 4. (Stretch) SQL server-wall investigation ### 4. (Stretch) SQL server-wall investigation — ❌ RegisterTags prime does NOT help (2026-06-22)
- `ExecuteSqlCommand` over gRPC faults server-side in `CSrvDbConnection.ExecuteSqlCommand` - `ExecuteSqlCommand` over gRPC faults server-side in `CSrvDbConnection.ExecuteSqlCommand`
(IndexOutOfRange / native err 38) — a DB-connection precondition the managed session (IndexOutOfRange / native err 38). Tried the `HistoryService.RegisterTags`-family prime
doesn't establish. Next avenue: try a `HistoryService.RegisterTags`-family prime before before `ExecuteSqlCommand` on both read-only (0x402) and write-enabled (0x401) sessions:
`ExecuteSqlCommand` (same fix that unblocked the M3 write path / OpenStorageConnection it does **not** clear the wall — `RegisterTags` itself returned false and `ExecuteSqlCommand`
class of wall). If it works, replace the bounded throw in `HistorianGrpcSqlClient` with faulted with the identical native-38 error (decoded buffer: `...CSrvDbConnection.ExecuteSqlCommand
the real GetNextQueryResultBuffer fetch loop (already written there) and flip the test. ... System.IndexOutOfRangeException`). So unlike OpenStorageConnection, the SQL DB-connection
context is NOT established by the RegisterTags family. The op stays bounded behind
`ProtocolEvidenceMissingException`; use WCF for SQL. Remaining avenues are deeper (reproduce
the server-side DB connection-string/index setup the native client triggers) — low priority.
### 5. (Optional) GetConnectionStatus over gRPC ### 5. GetConnectionStatus over gRPC — ✅ DONE 2026-06-22
- Currently WCF-only, synthesized from an authenticated probe (no dedicated RPC either - `HistorianGrpcStatusClient.GetConnectionStatusAsync` synthesizes the status from a measured
transport). Could synthesize the same over gRPC via `StatusService.PingServer` / gRPC handshake (OpenConnection yielding a storage-session GUID ⇒ connected), mirroring the WCF
`GetHistorianConsoleStatus`. Low value; do only if parity is wanted. synthesize-from-probe approach. Routed in `Historian2020ProtocolDialect` on `UseGrpc` (the WCF
path used the MDAS binding, which can't reach the gRPC port). Live-verified; store-forward
connectivity stays false (D2-gated). Gated test `GetConnectionStatusAsync_OverGrpc_ReportsConnected`.
### Out of scope ### Out of scope
- `ReadBlocks` (`StartBlockRetrievalQuery`) — never captured on either transport; leave - `ReadBlocks` (`StartBlockRetrievalQuery`) — never captured on either transport; leave