fix(historian): #686 serve reverse/end-only raw reads on forward-only backends (Gateway)
#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.
This commit is contained in:
@@ -56,6 +56,14 @@ public abstract class RawReadConformance : ConformanceBase
|
||||
Values(await B.CreateDataSource().ReadRawAsync("A", At(10), max, 0, Ct)).ShouldBe([20d, 30d, 40d, 50d]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_backend_reports_reverse_raw_support()
|
||||
{
|
||||
// #686: the server only sends start > end to a backend that declares it; these backends serve it natively.
|
||||
using var src = B.CreateDataSource();
|
||||
src.SupportsReverseRaw.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reverse_read_is_descending_over_end_exclusive_start_inclusive()
|
||||
{
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user