All three alarm-history writers inserted one event per round trip, serially
(SqlServer also opened a pooled connection per event), so a 10000-event
alarm storm drained at 10000 x RTT while the backlog sat in LocalDb.
- Sql.Common.AlarmBatchWrite: one statement per chunk; a chunk that fails
PermanentFail-class is re-sent event by event so only the poison event
dead-letters; a transient failure retries the chunk; a cancellation never
dead-letters.
- SqlServer: multi-row insert-if-absent over a VALUES set (150 events per
statement, under the 2100-parameter limit), duplicate ids in a batch
written once; a duplicate-key race isolates per event, where it is Ack.
No new table type, so no schema migration (the review suggested a TVP;
the VALUES form gets the same one-round-trip-per-chunk without one).
- QuestDb: multi-row INSERTs of 500 events; pre-1970 events still
dead-letter unsent.
- Gateway: no batch RPC exists, so sends are pipelined, at most 8 in
flight, outcomes kept in input order.
Rebased from the unpushed remed/lane-H2 branch (originally authored 2026-10-22
in session_01Ub9hGmYLQvn9XETzw7CMsT) onto current master.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
A capped Gateway HistoryReadEvents read broke out of the gRPC stream as soon
as the page was full. Abandoning a gateway stream cancels the RPC, which the
gateway treats as a connection fault: it evicts its pooled historian session,
so the next read reconnects and concurrent reads on that session fail.
The read now keeps consuming the stream past the cap without collecting it,
bounded by ServerHistorian:Gateway:EventDrainBudget (default 10000 rows; 0 =
the old abandon-at-cap). Rows of a foreign source never count toward the
budget before the "more events" signal is seen, so an abandoned stream never
answers a page as complete. A paged-vs-unpaged fuzz (random tie series,
foreign rows, caps 1..500, budgets 0..10000) pins the page contents and the
drain; it found that abandon-before-signal case.
ReadEventsAsync and the native AtTime arm now rethrow a caller cancellation
instead of recording it as a backend failure.
Rebased from the unpushed remed/lane-H2 branch (originally authored 2026-10-22
in session_01Ub9hGmYLQvn9XETzw7CMsT) onto current master.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
GatewayHistorianDataSource.ReadEventsAsync sized its result list to
maxEvents, which is int.MaxValue for NumValuesPerNode = 0: every
uncapped Gateway events read (forward on master, and the new walked
reverse one) threw 'Array dimensions exceeded' and counted as a backend
failure. Capacity is bounded at 1024. The events walk delegate moved to
ReverseEventsReader so it is tested over the real Gateway source
(forward uncapped, reverse uncapped, reverse capped).
Average, Count, Minimum, Maximum, Range, Start, End, Delta and Minimum/MaximumActualTime are evaluated
on the Gateway backend from forward ReadRaw reads with BucketPlan + AggregateEvaluator, so Gateway
matches SqlServer/QuestDB exactly (values, status bits, interval-start stamps, #677 reverse).
- Window read [start - 1 ms, end] so AVEVA's initial-value row never lands in a bucket; a pre-window
row proves earlier data only with a non-Bad OPC quality (histsdk puts a value on every NULL row).
- Earlier data otherwise from a 2-row [1970, start) probe; later data from a 2-row [end, now+1d]
look-ahead; both ignore rows stamped at their query start/end.
- No gateway stream is ever abandoned (a client-side cancel makes the gateway evict its shared
historian session); past ServerHistorian:MaxTimeWeightedRawRows the bounded stream is drained, then
BadTooManyOperations. The gateway's max_values-ceiling InvalidArgument maps to the same limit; a
caller cancellation is not a backend fault.
- TimeAverage/Total stay AVEVA-native (no prior-sample read for exact Part 13 bounds), now stamped with
the interval start (PENDING the live leg: histsdk's tail+43 start_time field is unverified) and
carrying the Calculated bits.
- Served set: GatewayHistorianDataSource.ServedAggregates (12). Live AVEVA leg open (VPN).
Addresses Task 1 code-review: document that ReadEventsAsync.maxEvents is enforced
client-side (no server cap in the wire contract); add Platforms=AnyCPU;x64 to match
sibling drivers; use ValueTask.CompletedTask in FakeHistorianGatewayClient.
Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii