410c8d8976
#669 turned an end-only capped HistoryRead-Raw into a reverse read (start > end) for every backend; the gateway rejects start > end (InvalidArgument), so on the default Gateway backend every end-only capped and every explicit reverse raw read failed the node and was counted as a backend health failure. - IHistorianDataSource.SupportsReverseRaw (default false; SqlServer/QuestDB/Null true, Gateway false; guard test pins that every production backend declares it). - OtOpcUaNodeManager sends a reverse window only to a backend that declares it; otherwise ReverseRawReader walks (lo, hi] back in bounded forward segment reads (1 min, x4 while sparse; each <= MaxTieClusterOverfetch + 1 rows; a full read is re-read narrower, never answered truncated; filtered on sample timestamps, which also drops AVEVA's fabricated query-start rows; stops at the Unix epoch). A one-timestamp tie cluster over the bound => BadTooManyOperations. - ServeRawPagedAsync maps HistoryReadLimitExceededException to BadTooManyOperations. - GatewayHistorianDataSource.ReadRawAsync refuses start > end unsent, without recording a health failure.
46 lines
1.9 KiB
C#
46 lines
1.9 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
|
|
|
|
/// <summary>
|
|
/// Gitea #686: the gateway's <c>ReadRaw</c> streams ascending only and rejects <c>start > end</c> with
|
|
/// <c>InvalidArgument</c>, which used to be recorded as a backend health failure. The source declares it cannot read
|
|
/// newest-first (so the server walks back with forward reads), and refuses a reverse window itself without sending
|
|
/// anything or counting a failure.
|
|
/// </summary>
|
|
public sealed class GatewayRawDirectionTests
|
|
{
|
|
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
|
|
|
[Fact]
|
|
public void The_gateway_source_declares_it_cannot_read_raw_newest_first()
|
|
{
|
|
IHistorianDataSource ds = new GatewayHistorianDataSource(
|
|
new FakeHistorianGatewayClient(), NullLogger<GatewayHistorianDataSource>.Instance);
|
|
ds.SupportsReverseRaw.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_reverse_raw_window_is_refused_unsent_and_is_not_a_backend_failure()
|
|
{
|
|
var fake = new FakeHistorianGatewayClient();
|
|
IHistorianDataSource ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance);
|
|
var end = new DateTime(2026, 10, 12, 18, 7, 45, DateTimeKind.Utc);
|
|
|
|
await Should.ThrowAsync<NotSupportedException>(() =>
|
|
ds.ReadRawAsync("Gate681", end, new DateTime(0, DateTimeKind.Utc), 3, Ct));
|
|
|
|
fake.ReadRawCallCount.ShouldBe(0);
|
|
var health = ds.GetHealthSnapshot();
|
|
health.TotalFailures.ShouldBe(0);
|
|
health.ConsecutiveFailures.ShouldBe(0);
|
|
|
|
// Positive control: a forward read of the same window is sent.
|
|
await ds.ReadRawAsync("Gate681", new DateTime(2026, 10, 12, 18, 0, 0, DateTimeKind.Utc), end, 3, Ct);
|
|
fake.ReadRawCallCount.ShouldBe(1);
|
|
}
|
|
}
|