Files
histsdk/docs/reverse-engineering/event-session-reuse-spike-results.md
Joseph Doherty 5f949a86e2 docs(spike): event-session reuse spike results — GREEN
v8 Event session reuses across SendEvent (~10-16x amortization); register-once
sufficient; session survived 25s idle; event reads stay gated (C2). Live-validated
against wonder-sql-vd03. Gate decision: GREEN -> HistorianGateway Stage B1 (separate
event-session pool for SendEvent) warranted.

Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
2026-06-25 11:34:34 -04:00

112 lines
6.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Event-session reuse spike — live results
> **Question:** does the 2023 R2 historian honor REUSING one authenticated **v8 Event**
> session (ECDH `ExchangeKey` → RC4 token → `ConnectionType=Event`, then `RegisterCmEventTag`)
> across multiple `SendEvent` ops, instead of the per-op open+register the SDK does today?
> This is the precondition for amortizing the EVENT path (HistorianGateway `pending.md` A1
> broadening, Stage B0 / B1).
>
> **Verdict: GREEN — a v8 Event session reuses across sends, register-once is sufficient,
> and the amortization is ~1016×. Event READS stay gated (C2) and are not a reuse signal.**
**Date:** 2026-06-25
**Branch:** `feat/amortization-broadening`
**Server:** live 2023 R2 (`wonder-sql-vd03`), RemoteGrpc transport.
**Sandbox identity:** `HISTORIAN_EVENT_SANDBOX_TAG=HistGW.LiveTest.EventSpike` — the CM_EVENT send
buffer has **no per-tag routing field** (it registers against a fixed system tag), so the sandbox
value is stamped into the event `Type`/`SourceName`/`Namespace` + a `SpikeMarker` property as an
**identity marker**; no real tag is written or overwritten.
**Harness:** `tests/AVEVA.Historian.Client.Tests/EventSessionReuseSpikeTests.cs` driving the B0a
seams `HistorianGrpcEventWriteOrchestrator.OpenAndRegisterEventSession` (open v8 Event session +
`RegisterCmEventTag` ONCE) and `SendEventOnSession` (send only — no open/register).
---
## 1. Send reuse — GREEN
`ReusedEventSession_SendsTwice_SecondSkipsHandshake` **passed** (both runs): one
`OpenAndRegisterEventSession` then **two `SendEventOnSession` on the same v8 Event session** — both
accepted (`AddStreamValues` `BSuccess=true`).
```
open+register (ECDH handshake + RegisterCmEventTag) = 242 ms (run 1: 350 ms)
registration diag: RTag=True; EnsT=True
reused-send[0] = 23 ms, ok=True
reused-send[1] = 22 ms, ok=True
```
The server accepts the same v8 Event client handle across back-to-back sends. The session handle is
an immutable `readonly record struct (uint ClientHandle, Guid StorageSessionId)`; the send is
stateless on the client side (each call reserializes a fresh `"OS"` buffer), so nothing per-op is
baked into the handle.
## 2. Amortization — ~1016×
The open+register (P-256 ECDH `ExchangeKey` → RC4 credential token → v8 `OpenConnection`
`RegisterCmEventTag`) costs ~242350 ms and is paid **once**; a reused send is ~22 ms. So over a
burst of N sends the per-send cost collapses from ~(265 ms open + 22 ms) to ~22 ms — a ~1016× win
on the send path, same shape as the v6 read/write amortization (`handshake-reuse-spike-results.md`).
## 3. Register-once is sufficient — GREEN
`ReusedEventSession_RegisterOnce_ThenSendMany` **passed**: `RegisterCmEventTag` run **once** (inside
`OpenAndRegisterEventSession`), then **three** sends, all accepted.
```
register-once send[0] = 25 ms, ok=True
register-once send[1] = 22 ms, ok=True
register-once send[2] = 22 ms, ok=True
```
CM_EVENT registration is **session-scoped, not per-send** — the server holds the registration for the
session's lifetime. A reuse pool registers once per warm session, not per op.
## 4. Idle tolerance — survived ≥25 s (best-effort, single sample)
`ReusedEventSession_IdleSweep_BestEffort` (log-only): after a send, a **25 s idle gap**, then another
send — **the second send succeeded** (`session SURVIVED the idle gap`). Notable: the v6 read session
idle-expires at a ≥25 s gap (`handshake-reuse-spike-results.md` §3), but this v8 Event session
survived 25 s. This is a single-sample best-effort observation — a keepalive should still ping under
the ~20 s floor for safety margin until the v8 Event idle boundary is characterized more finely.
## 5. Read-after-send — GATED (C2), not a reuse signal
`ReusedEventSession_ServesReadAfterSend_BestEffort` (log-only, hard-bounded by a 5 s gRPC deadline +
an 8 s cancellation): the read-after-send on the same session **did not return data** — it cancels at
the bound:
```
read-after-send -> swallowed (RpcException Cancelled / OperationCanceled)
=> read gated/unverified over gRPC (expected)
```
This matches the pre-existing C2 gate: event **reads** over gRPC long-poll `GetNext` to a no-data
terminal and are unverified. So the spike did **not** prove a one-session-serves-both-kinds property
for reads — `SendEvent` is the only trustworthy reuse signal. (An unbounded read hung the first run;
the harness now bounds it so the spike is a clean, re-runnable record.)
---
## 6. Implications for Stage B1 (the event-pool build)
GREEN → a **separate event-session pool** (the approved B1 approach) is warranted and high-value:
1. **Amortize `SendEvent` through a bounded event-session pool.** Open+register a v8 Event session
once per warm session; lease it per send op (exclusive, like the v6 pool); reuse across a burst.
~1016× on the send path.
2. **Keep the event pool SEPARATE from the v6 pool** (B1, as approved) — different auth (ECDH/v8),
heavier re-handshake on drop, and its own idle characteristics.
3. **`ReadEvents` stays PER-CALL / gated (C2).** Reads are unverified over gRPC regardless of reuse,
so the event pool amortizes **sends only**; `ReadEvents` is unaffected by B1 and stays on the
per-call path. (This refines the design's "route SendEvent + ReadEvents through the pool": only
`SendEvent` is routed; `ReadEvents` remains per-call because it is gated, not because of reuse.)
4. **Keepalive:** ping the warm event session under the idle floor. The cheap keepalive op for the
event channel is TBD in B1 (the v6 pool uses `GetSystemParameter`; the event session's equivalent
warm-touch needs picking — likely a no-op send or a lightweight event-channel status op).
5. **Reactive re-auth:** on an expiry-looking failure, evict + full v8 re-handshake (heavier than the
v6 re-auth — one ECDH + register penalty).
**Gate decision: GREEN → HistorianGateway A1 Stage B1 (a bounded `HistorianEventSessionPool` for
`SendEvent`, default-on, parallel to the v6 `HistorianSessionPool`) is warranted and earns its own
re-planned design + plan.**