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:
+7
-2
@@ -9,8 +9,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer;
|
||||
/// <summary>
|
||||
/// <see cref="IHistorianValueWriter"/> over the SQL <c>Sample</c> table. Redelivery-idempotent: rows are
|
||||
/// inserted only where <c>(TagId, SourceTimeUtc)</c> is absent, so the recorder's at-least-once redelivery
|
||||
/// leaves the first-stored sample untouched. Never throws; returns false on any failure so the recorder keeps
|
||||
/// the entries and its per-tag quarantine keeps working.
|
||||
/// leaves the first-stored sample untouched. Returns false on any failure so the recorder keeps the entries and its
|
||||
/// per-tag quarantine keeps working — except a cancellation of the caller's token, which is rethrown: it ends the
|
||||
/// PASS, and booked as a tag failure it could tip a healthy tag over its attempt budget.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Non-finite doubles.</b> NaN / ±Infinity cannot live in a SQL <c>float</c> (the TVP throws). They are stored
|
||||
@@ -90,6 +91,10 @@ internal sealed class SqlServerHistorianValueWriter : IHistorianValueWriter
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
throw; // the drain pass was cancelled (shutdown) — not a write failure of this tag
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogWarning("SQL historian live-value write failed ({Exception}); will retry.", ex.GetType().Name);
|
||||
|
||||
Reference in New Issue
Block a user