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

6.0 KiB
Raw Permalink Blame History

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 OpenConnectionRegisterCmEventTag) 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.