Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.Tests/SqlServerValueWriterCancellationTests.cs
T
Joseph Doherty 7c5cc9c7e5 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
2026-11-09 15:40:46 -05:00

38 lines
1.6 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.Tests;
/// <summary>
/// The recorder's contract: a cancelled drain pass rethrows <see cref="OperationCanceledException"/> out of the writer —
/// swallowed as <c>false</c> it would be booked as a tag failure and could tip a healthy tag over its attempt budget.
/// Any other failure is still <c>false</c>. The connection points at a closed port, so nothing reaches a server.
/// </summary>
public sealed class SqlServerValueWriterCancellationTests
{
private static SqlServerHistorianValueWriter Writer()
{
var opts = new SqlServerHistorianOptions { ConnectionString = "Server=127.0.0.1,1;Connect Timeout=1;TrustServerCertificate=True" };
var conn = new SqlHistorianConnection(opts);
var boot = new SqlServerSchemaBootstrapper(conn, opts);
return new SqlServerHistorianValueWriter(conn, boot, new SqlServerTagCatalog(conn, boot));
}
private static readonly HistorizationValue[] One = [new(DateTime.UtcNow, HistorianValue.FromDouble(1), 0)];
[Fact]
public async Task A_cancelled_pass_rethrows()
{
using var cts = new CancellationTokenSource();
await cts.CancelAsync();
await Should.ThrowAsync<OperationCanceledException>(() => Writer().WriteLiveValuesAsync("T", One, cts.Token));
}
[Fact]
public async Task An_unreachable_server_is_a_failed_write_not_a_throw()
{
(await Writer().WriteLiveValuesAsync("T", One, TestContext.Current.CancellationToken)).ShouldBeFalse();
}
}