From c6d4134795e99af12e1c95fdd514c53551c88c8b Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 9 Oct 2026 14:24:45 -0400 Subject: [PATCH] test(historian): gateway writer acks an all-non-numeric batch without a call; fix stale writer docs Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe --- .../Historian/IHistorianValueWriter.cs | 20 +++++++++---------- .../GatewayHistorianValueWriterTests.cs | 18 +++++++++++++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianValueWriter.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianValueWriter.cs index 50dd3ed5..91e04b5f 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianValueWriter.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianValueWriter.cs @@ -1,13 +1,13 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// -/// One numeric sample the continuous-historization recorder drains to the historian's SQL -/// live-value write path: an optional UTC timestamp, the typed value, and the OPC UA status code -/// the sample was recorded with. +/// One sample the continuous-historization recorder drains to the backend's live-value write path: an +/// optional UTC timestamp, the typed value (any the backend supports — the +/// Gateway backend takes numeric kinds only, SqlServer takes all of them), and the OPC UA status code the +/// sample was recorded with. /// /// -/// UTC source timestamp of the sample, or null to defer to the historian's server-stamped -/// current-time path (the SQL writer uses SYSDATETIME() when the timestamp is absent). +/// UTC source timestamp of the sample, or null to have the backend stamp the current time. /// /// The typed sample value. /// OPC UA status code of the sample (0 = Good). @@ -16,20 +16,20 @@ public readonly record struct HistorizationValue(DateTime? TimestampUtc, Histori /// /// Seam over the historian's live-value write path used by the continuous-historization recorder. /// Lives in the abstraction layer so the Runtime recorder depends on it without taking a hard -/// reference on the gRPC gateway driver; the gateway driver supplies the concrete adapter -/// (GatewayHistorianValueWriter). +/// reference on a backend driver; the selected backend provider supplies the concrete writer +/// (GatewayHistorianValueWriter or SqlServerHistorianValueWriter). /// public interface IHistorianValueWriter { /// - /// Writes a batch of live values for a single tag through the historian's SQL live-write path. + /// Writes a batch of live values for a single tag through the backend's live-write path. /// Implementations are expected to be non-throwing: a transport/gateway error is surfaced as a /// false result so the recorder retains the entries and retries, rather than as an /// exception. /// /// Fully-qualified historian tag the values are recorded against. - /// The numeric samples to write, in append order. + /// The samples to write, in append order. /// Cancellation token. - /// true on a successful (or durably-queued) gateway ack; false on a retryable failure. + /// true when the backend stored (or durably queued) the batch; false on a retryable failure. Task WriteLiveValuesAsync(string tag, IReadOnlyList values, CancellationToken ct); } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Recorder/GatewayHistorianValueWriterTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Recorder/GatewayHistorianValueWriterTests.cs index 091fb40b..6029cb96 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Recorder/GatewayHistorianValueWriterTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Recorder/GatewayHistorianValueWriterTests.cs @@ -73,6 +73,24 @@ public sealed class GatewayHistorianValueWriterTests Assert.False(ok); } + [Fact] + public async Task All_non_numeric_batch_is_acked_without_calling_the_gateway() + { + // A soft-failure ack: had the writer called the gateway, the result would be false. + var fake = new FakeHistorianGatewayClient { WriteLiveValuesResult = new WriteAck { Success = false, Queued = false } }; + var values = new[] + { + new HistorizationValue(DateTime.UtcNow, HistorianValue.FromString("text"), 0u), + new HistorizationValue(DateTime.UtcNow, HistorianValue.FromDateTime(DateTime.UtcNow), 0u), + new HistorizationValue(DateTime.UtcNow, HistorianValue.Empty, 0x80310000u), + }; + + var ok = await Writer(fake).WriteLiveValuesAsync("T", values, TestContext.Current.CancellationToken); + + Assert.True(ok); + Assert.Equal(0, fake.WriteLiveValuesCallCount); + } + [Fact] public async Task Empty_batch_is_trivially_acked_without_calling_the_gateway() {