diff --git a/docs/reverse-engineering/grpc-event-query-capture.md b/docs/reverse-engineering/grpc-event-query-capture.md index fdab918..272659f 100644 --- a/docs/reverse-engineering/grpc-event-query-capture.md +++ b/docs/reverse-engineering/grpc-event-query-capture.md @@ -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 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 `tools/AVEVA.Historian.Grpc2023CaptureHarness` gained a `capture-event` scenario. It loads the diff --git a/docs/reverse-engineering/wcf-event-read-spike-results.md b/docs/reverse-engineering/wcf-event-read-spike-results.md index 1f4ad98..c5fd5df 100644 --- a/docs/reverse-engineering/wcf-event-read-spike-results.md +++ b/docs/reverse-engineering/wcf-event-read-spike-results.md @@ -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 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). + +## 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). diff --git a/tests/AVEVA.Historian.Client.Tests/HistorianGrpcIntegrationTests.cs b/tests/AVEVA.Historian.Client.Tests/HistorianGrpcIntegrationTests.cs index ce47b55..abc163a 100644 --- a/tests/AVEVA.Historian.Client.Tests/HistorianGrpcIntegrationTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/HistorianGrpcIntegrationTests.cs @@ -517,19 +517,26 @@ public sealed class HistorianGrpcIntegrationTests } // Plan #2: ReadEvents over gRPC. The chain runs end-to-end and StartEventQuery succeeds - // (no InvalidOperationException), but — confirmed live 2026-06-22 — GetNextEventQueryResultBuffer - // LONG-POLLS and returns zero rows: the gRPC server blocks to the deadline instead of - // returning the synchronous 5-byte code-85 terminal the 2020 WCF op returns, so the orchestrator - // reaches its no-data terminal with zero rows and (rather than assert a possibly-false "no events" - // empty) throws ProtocolEvidenceMissingException. + // (no InvalidOperationException), but GetNextEventQueryResultBuffer LONG-POLLS and returns zero + // rows: the gRPC server blocks to the deadline instead of returning the synchronous 5-byte + // code-85 terminal the 2020 WCF op returns, so the orchestrator reaches its no-data terminal + // with zero rows and (rather than assert a possibly-false "no events" empty) throws + // ProtocolEvidenceMissingException. // - // IMPORTANT (2026-06-22): the zero rows are NOT "no events on the server". Verified against the - // live 2023 R2 box, which holds 19,356 events in the last 30 days (SQL ground truth via the INSQL - // linked server, Runtime.dbo.Events). The EMPTY-FILTER gRPC event query simply does not match - // them. So the gate here is the empty-filter request shape (filter / namespace / event-tag - // registration), NOT data availability — this is capture-gated (needs a native gRPC event-query - // capture), not server-gated. Flip to asserting parsed rows once that capture lands and the - // request is corrected. The chain stays BOUNDED (no multi-minute hang) via the short + // SERVER-GATED on 2023 R2 (settled 2026-06-26; supersedes a 2026-06-22 draft that read the zero + // rows as a client-side empty-filter/request-shape "capture gate"). Three live controls against + // the same event-bearing 2023 R2 server retire the request-shape theory: + // 1. The STOCK AVEVA 2023 R2 client (aahClientManaged, native Event connection, its OWN correct + // event query — not our empty-filter shape) returns 0 rows over 30d / 90d / 365d / 3yr. + // The 2026-06-22 "50 rows" stock capture did NOT reproduce. + // 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.) HistorianClient client = new(BuildOptions(host));