fix(historian): live writers rethrow pass cancellation; gateway writes normalised (lmxopcua#746)

Every IHistorianValueWriter caught Exception and returned false, including
OperationCanceledException from the drain's own token. The recorder's guard
("a cancelled pass is not a tag failure") was therefore dead code: at shutdown
mid-pass each cancelled group could be booked as a consecutive failure and,
with a sibling that already wrote, tip a healthy tag toward quarantine.
SqlServer, QuestDb and Gateway writers now rethrow a cancellation of the
caller's token; a cancellation-shaped error on a live token is still an
ordinary failed write.

The Gateway writer relabelled a Local-kind timestamp as UTC instead of
converting it, so the same value could land at two different times on two
backends. Stamped samples now go through LiveWritePreparer (Local -> UTC,
non-finite doubles not sent, duplicate timestamps keep the first); unstamped
samples are still left for the gateway to stamp, in batch 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
This commit is contained in:
Joseph Doherty
2026-10-22 16:42:13 -04:00
parent bc7e58ed64
commit 7c5cc9c7e5
7 changed files with 204 additions and 29 deletions
+7
View File
@@ -219,6 +219,13 @@ source it from there.
> it is sent (a server-managed column; 0.2.0 Contracts C-002), so **AVEVA still stores every recorded value as
> Good** — a pre-existing limitation of that backend, now documented rather than fixed (archreview 06/C-7). The
> `SqlServer` backend stores the real status.
>
> **Live writes are normalised alike on every backend.** The Gateway writer now runs each stamped sample through the SQL
> backends' `LiveWritePreparer`: a `Local`-kind timestamp is **converted** to UTC (it used to be relabelled UTC, shifting it by
> the node's offset), NaN / ±Infinity are not sent (the SQL backends store them as an Empty + Bad sample; the gateway's live
> path is numeric-only), and a duplicate timestamp within a batch keeps the first. An unstamped sample is still left for the
> gateway to stamp. Every live writer returns `false` for a failed write but **rethrows a cancellation of the drain pass**
> (shutdown), so a node stop can never be booked as a tag failure toward the #538 quarantine.
> ✅ **Continuous value capture is LIVE-PROVEN END-TO-END
> ([#491](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/491) CLOSED — gate run 2026-08-01,