SendEvent over gRPC: implement + live-validate (was capture-gated)

Captured the native 2023 R2 client's gRPC event send (new capture-send-event
harness scenario): it rides HistoryService.AddStreamValues with the SAME "OS"
(0x534F) storage-sample buffer the WCF path already uses (HistorianEventWrite-
Protocol) — confirming "no distinct RPC", and that it is NOT the historical
write's "ON" buffer. Diffed the write-enabled vs read-only v8 Event open: byte-
identical apart from per-session crypto, so the existing OpenSession event path
is reused unchanged.

So SendEvent-over-gRPC was pure assembly of proven parts:
- HistorianGrpcEventWriteOrchestrator = v8 Event open + CM_EVENT registration
  (UpdC3/RegisterTags/EnsureTags) + AddStreamValues(OS buffer).
- HistorianClient.SendEventAsync now routes to it for RemoteGrpc (WCF otherwise).

Live-validated end-to-end against the 2023 R2 server: pure-managed SDK send →
AddStreamValues BSuccess=true → the event reads back from the server (markers
confirmed in returned event rows). The native gRPC RegisterTags(24B) +
EnsureTags(86B) byte-match our serializers (new GrpcEventSendProtocolTests
golden, closing the 83-vs-86 EnsureTags question). Gated live test
SendEventAsync_OverGrpc_AcceptsEvent (opt-in HISTORIAN_GRPC_EVENT_SEND=1).
331 offline tests pass.

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-23 15:37:22 -04:00
parent ae536bb4b8
commit afc7c4bf96
6 changed files with 384 additions and 21 deletions
+22 -13
View File
@@ -76,7 +76,17 @@ reuses the proven 2020 WCF byte serializers/parsers unchanged inside protobuf
`capture-event` harness (native, returns rows).
2. **R4.3 active-SF magnitude** — needs an **SF-active server** (D2 storage-engine
console handle).
3. **SendEvent over gRPC****capture-gated**: no distinct RPC, framing uncaptured.
3. **SendEvent over gRPC** **SHIPPED + LIVE-VALIDATED 2026-06-23.** `SendEventAsync`
now routes over `RemoteGrpc` (`HistorianGrpcEventWriteOrchestrator`). Captured the native
client live (`capture-send-event` harness scenario): the send rides
`HistoryService.AddStreamValues` with the **same "OS" (0x534F) buffer the WCF path uses**
(`HistorianEventWriteProtocol` — "no distinct RPC" confirmed true), on a v8 Event session +
CM_EVENT registration. The write-enabled Event open is **byte-identical** to the read-only one
(diffed live — only per-session crypto differs), so the existing event-open path is reused
unchanged. End-to-end: pure-managed SDK send → `BSuccess=true` → event read back from the live
server (markers `SdkSendProbe`/`SdkCaptureProbe` confirmed in returned rows). Golden-tested
(`GrpcEventSendProtocolTests`) + gated live test (`SendEventAsync_OverGrpc_AcceptsEvent`,
opt-in `HISTORIAN_GRPC_EVENT_SEND=1`).
4. **ExecuteSqlCommand over gRPC****server-walled** (`CSrvDbConnection`;
RegisterTags prime doesn't help). Use WCF for SQL.
5. **R4.2 revision EDITS** — storage-engine-pipe-only on BOTH transports (the D2 wall).
@@ -123,18 +133,17 @@ with these refinements:
would differ is native and not on the wire. One untested low-effort check
remains: byte-diff a captured **Event-connection** EnsureTags/RegisterTags
against our replay (the 83-vs-86-byte EnsT gap was never actually compared).
- **Item 3 (SendEvent over gRPC)** — **sharpened from "maybe no RPC" to a precise
capture.** RPC **confirmed** = `HistoryService.AddStreamValues` (the "no distinct
RPC" note is TRUE; an event rides the same RPC as a streamed sample, discriminated
inside `btValues`). Public API `HistorianAccess.AddStreamedValue(HistorianEvent)`
→ native `AddHistorianValue`; prereqs known (write-enabled Event conn, CM_EVENT
tag handle, quality 192); field set/order recovered from `HistorianEvent.PackToVtq`.
**Only the `btValues` VTQ byte layout is missing** — built by native
`CCommonArchestraEventValue::PackToVtq` and copied out as an opaque `CDataChunk`.
Our read parser already decodes the inverse property-bag format. **Capturable
against the local Historian** (instrument `PackToVtq` output / the `AddStreamValues`
body) → then build `HistorianEventWriteProtocol` and reuse the
`HistorianGrpcHistoricalWriteOrchestrator` plumbing.
- **Item 3 (SendEvent over gRPC)** — **SHIPPED + LIVE-VALIDATED 2026-06-23** (was
"capturable"). RPC confirmed = `HistoryService.AddStreamValues` (the "no distinct RPC"
note is TRUE). The `btValues` VTQ buffer turned out to be already-owned: our M2
`HistorianEventWriteProtocol.SerializeAddStreamValuesBuffer` ("OS" buffer, decoded from
the WCF event-send) is the transport-independent `PackToVtq` equivalent and the gRPC send
uses it **verbatim** (live capture: sig `OS`/0x534F, CM_EVENT GUID, identical framing — NOT
the historical write's "ON" buffer). The write-enabled Event open is byte-identical to the
read-only one (live diff). So SendEvent-over-gRPC was pure assembly:
`HistorianGrpcEventWriteOrchestrator` = existing v8 Event open + existing CM_EVENT
registration + `AddStreamValues`(OS buffer). End-to-end live-validated (send → `BSuccess`
→ read back from the live server). Golden-tested + gated live test.
- **Item 4 (ExecuteSql over gRPC)** — **confirmed walled + explained.** The stock
client gates SQL **out client-side**: `HistorianAccess.ExecuteSqlCommand` returns
`OperationNotSupported` when `IsManagedHistorian(node)` or `!IsProcessConnectionRequested()`