1 Commits

Author SHA1 Message Date
Joseph Doherty 54bcf8a5e0 docs(c2): record 2020-vs-2023R2 positive control for the event-read server gate
Adds the positive control that the prior C2 evidence lacked. The SAME native WCF
event-read client returns real events (5) from a local AVEVA Historian 2020 but 0
from the 2023 R2 server over the identical sequence and window, while both boxes
hold tens of thousands of events in SQL — isolating the zero-rows to the 2023 R2
server, not the client, protocol, or serializers.

- wcf-event-read-spike-results.md: new "2026-06-26 positive control" section
  (2020 vs 2023 R2 A/B from one WCF client; stock-2020-client version-self-block
  caveat; stock-2023R2 gRPC cross-check).
- grpc-event-query-capture.md: re-control note — the 2026-06-22 stock 50-row
  capture did NOT reproduce; the stock 2023 R2 client now also returns 0 rows.
- HistorianGrpcIntegrationTests: correct the stale "capture-gated, NOT
  server-gated" comment to the server-gate conclusion backed by the controls.

Sanitized throughout (counts, native return codes, buffer lengths only).

Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
2026-06-26 11:08:20 -04:00
3 changed files with 59 additions and 12 deletions
@@ -4,6 +4,16 @@ Captured the stock 2023 R2 client performing a **gRPC event read** that returns
the open item "gRPC event ROW retrieval returns zero rows" (handoff §Current Status item 1). This the open item "gRPC event ROW retrieval returns zero rows" (handoff §Current Status item 1). This
closes the capture-gate: the working request shape is now known. closes the capture-gate: the working request shape is now known.
> **Re-control 2026-06-26 — this 50-row result did NOT reproduce.** Re-running the stock 2023 R2
> client (same `capture-event` harness, native Event connection) against the same server returned
> **0 rows** over 30d / 90d / 365d / 3yr, even though `Runtime.dbo.Events` held ~71.5k events in the
> window (newest minutes old). So the native gRPC event read is **server-gated**, not merely
> request-shape/"capture" gated — the gate also stops the stock client now. Positive control the same
> day: the same native **WCF** event-read client returns real events (5) from a local AVEVA Historian
> 2020 but 0 from this 2023 R2 server (see `wcf-event-read-spike-results.md` §"2026-06-26 — positive
> control"). The 50-row capture below remains valid as the *captured-correct request format*; treat its
> "returns rows" framing as a point-in-time observation that the server no longer honors.
## How it was captured ## How it was captured
`tools/AVEVA.Historian.Grpc2023CaptureHarness` gained a `capture-event` scenario. It loads the `tools/AVEVA.Historian.Grpc2023CaptureHarness` gained a `capture-event` scenario. It loads the
@@ -73,3 +73,33 @@ connection) — not client-fixable. **C2 stays closed won't-fix**, for this (cor
- The C2 spike is now transport-selectable (integrated|certificate), cross-platform for the cert - The C2 spike is now transport-selectable (integrated|certificate), cross-platform for the cert
transport, bounded (per-call timeout + overall budget with a phase-diagnostic dump), and version-gate transport, bounded (per-call timeout + overall budget with a phase-diagnostic dump), and version-gate
bypassable. Output stays sanitized (counts, native return codes, buffer lengths, sha256). bypassable. Output stays sanitized (counts, native return codes, buffer lengths, sha256).
## 2026-06-26 — positive control: same WCF client, 2020 historian vs 2023 R2
The earlier evidence triangulated the gate but lacked a clean *positive* control — proof that the
native event-query path returns rows for **some** historian, so that the 0-row 2023 R2 result can be
attributed to the server rather than to the client/protocol. This run supplies it, A/B against two
historians from the **same** WCF event-read client (`HistorianWcfEventOrchestrator`, whose wire
protocol is byte-replayed from stock 2020 captures), same 365-day window:
| target historian | transport | RegisterTags (RTag2) | result buffer | events |
|---|---|---|---|---|
| **local AVEVA Historian 2020** | WCF, integrated | **0 — success** | terminal after rows | **5** |
| **2023 R2** (the C2 server) | WCF, certificate | (gate, as documented) | 10-byte 0-row header → long-poll | **0** |
SQL ground truth (`Runtime.dbo.Events`) for the same two boxes: the 2020 historian holds ~51.6k events
over the window, the 2023 R2 holds ~71.5k — both populated. So the **identical native WCF event-read
client returns real events from a 2020 historian and zero from the 2023 R2 server**. That isolates the
zero-rows to the 2023 R2 server: not the client, not the protocol, not our serializers.
Notes / honesty caveats:
- The genuinely-**stock** 2020 client (`aahClientManaged.dll` v2020.0406.2652.2, driven by reflection)
could **not** be run end-to-end here: against the local 2020 historian (services patched to build
3383.3) it self-blocks at `StartEventQuery` with `Invalid InterfaceVersion` (242) — a client-side
build/version gate, and the stock client has no version-bypass. Our client (which *does* bypass the
version check and byte-replays the same native sequence) is the faithful proxy that reaches the rows.
- Cross-check on the gRPC leg the same day: the **stock 2023 R2** client (native Event connection, its
own correct event query) returned **0 rows** over 30d/90d/365d/3yr against the 2023 R2 server; the
2026-06-22 "50 rows" stock capture did not reproduce. Same server-gate, both transports, both clients.
- Output sanitized throughout (counts, native return codes, buffer lengths only — no event identity,
host, or credentials).
@@ -517,19 +517,26 @@ public sealed class HistorianGrpcIntegrationTests
} }
// Plan #2: ReadEvents over gRPC. The chain runs end-to-end and StartEventQuery succeeds // Plan #2: ReadEvents over gRPC. The chain runs end-to-end and StartEventQuery succeeds
// (no InvalidOperationException), but — confirmed live 2026-06-22 — GetNextEventQueryResultBuffer // (no InvalidOperationException), but GetNextEventQueryResultBuffer LONG-POLLS and returns zero
// LONG-POLLS and returns zero rows: the gRPC server blocks to the deadline instead of // rows: the gRPC server blocks to the deadline instead of returning the synchronous 5-byte
// returning the synchronous 5-byte code-85 terminal the 2020 WCF op returns, so the orchestrator // code-85 terminal the 2020 WCF op returns, so the orchestrator reaches its no-data terminal
// reaches its no-data terminal with zero rows and (rather than assert a possibly-false "no events" // with zero rows and (rather than assert a possibly-false "no events" empty) throws
// empty) throws ProtocolEvidenceMissingException. // ProtocolEvidenceMissingException.
// //
// IMPORTANT (2026-06-22): the zero rows are NOT "no events on the server". Verified against the // SERVER-GATED on 2023 R2 (settled 2026-06-26; supersedes a 2026-06-22 draft that read the zero
// live 2023 R2 box, which holds 19,356 events in the last 30 days (SQL ground truth via the INSQL // rows as a client-side empty-filter/request-shape "capture gate"). Three live controls against
// linked server, Runtime.dbo.Events). The EMPTY-FILTER gRPC event query simply does not match // the same event-bearing 2023 R2 server retire the request-shape theory:
// them. So the gate here is the empty-filter request shape (filter / namespace / event-tag // 1. The STOCK AVEVA 2023 R2 client (aahClientManaged, native Event connection, its OWN correct
// registration), NOT data availability — this is capture-gated (needs a native gRPC event-query // event query — not our empty-filter shape) returns 0 rows over 30d / 90d / 365d / 3yr.
// capture), not server-gated. Flip to asserting parsed rows once that capture lands and the // The 2026-06-22 "50 rows" stock capture did NOT reproduce.
// request is corrected. The chain stays BOUNDED (no multi-minute hang) via the short // 2. SQL ground truth (Runtime.dbo.Events) shows the server holds tens of thousands of events
// in those windows (newest minutes old) — so it is emphatically NOT "no events".
// 3. POSITIVE CONTROL: the same native WCF event-read client (protocol byte-replayed from stock
// 2020 captures) returns real events (5) from a local AVEVA Historian 2020 over the identical
// sequence, but 0 (10-byte 0-row header + long-poll) from this 2023 R2 server.
// The protocol works; the 2023 R2 server simply does not scope event rows to a managed connection.
// Not client-fixable (see docs/reverse-engineering/grpc-event-query-capture.md +
// wcf-event-read-spike-results.md). The chain stays BOUNDED (no multi-minute hang) via the short
// registration + poll deadlines. (Set a small HISTORIAN_GRPC_TIMEOUT to keep this snappy.) // registration + poll deadlines. (Set a small HISTORIAN_GRPC_TIMEOUT to keep this snappy.)
HistorianClient client = new(BuildOptions(host)); HistorianClient client = new(BuildOptions(host));