fix(historian): #691.4 review — uncapped Gateway events reads no longer pre-size to int.MaxValue
GatewayHistorianDataSource.ReadEventsAsync sized its result list to maxEvents, which is int.MaxValue for NumValuesPerNode = 0: every uncapped Gateway events read (forward on master, and the new walked reverse one) threw 'Array dimensions exceeded' and counted as a backend failure. Capacity is bounded at 1024. The events walk delegate moved to ReverseEventsReader so it is tested over the real Gateway source (forward uncapped, reverse uncapped, reverse capped).
This commit is contained in:
+4
-1
@@ -54,6 +54,9 @@ public sealed class FakeHistorianGatewayClient : IHistorianGatewayClient
|
||||
public int LastReadEventsMaxEvents;
|
||||
public int ReadEventsCallCount;
|
||||
|
||||
/// <summary>When set, ReadEvents serves <c>EventsSource(start, end)</c> instead of <see cref="Events"/>.</summary>
|
||||
public Func<DateTime, DateTime, IEnumerable<HistorianEvent>>? EventsSource;
|
||||
|
||||
/// <summary>Thrown (deferred to first enumeration) by every read method when set.</summary>
|
||||
public Exception? ThrowOnRead;
|
||||
|
||||
@@ -187,7 +190,7 @@ public sealed class FakeHistorianGatewayClient : IHistorianGatewayClient
|
||||
LastReadEventsEndUtc = endUtc;
|
||||
LastReadEventsMaxEvents = maxEvents;
|
||||
ReadEventsCallCount++;
|
||||
return ToAsyncStream(Events, ThrowOnRead, ct);
|
||||
return ToAsyncStream(EventsSource is null ? Events : EventsSource(startUtc, endUtc).ToList(), ThrowOnRead, ct);
|
||||
}
|
||||
|
||||
public Task<WriteAck> WriteLiveValuesAsync(
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #691.4 review: events read uncapped (and walked back newest-first) through the REAL
|
||||
/// <see cref="GatewayHistorianDataSource"/> over a gateway-shaped fake — inclusive <c>[start, end]</c>, ordered by event time.
|
||||
/// </summary>
|
||||
public sealed class GatewayReverseEventsTests
|
||||
{
|
||||
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
||||
|
||||
private static readonly DateTime T0 = new(2026, 10, 12, 18, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
private static FakeHistorianGatewayClient Store(int count)
|
||||
{
|
||||
var stored = Enumerable.Range(0, count)
|
||||
.Select(i => new HistorianEvent { Id = $"e{i:0000}", Type = "Alarm", EventTime = Timestamp.FromDateTime(T0.AddSeconds(i)) })
|
||||
.ToList();
|
||||
return new FakeHistorianGatewayClient
|
||||
{
|
||||
EventsSource = (start, end) => stored.Where(e => e.EventTime.ToDateTime() >= start && e.EventTime.ToDateTime() <= end),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>NumValuesPerNode = 0</c> reaches the source as <c>int.MaxValue</c>; the result list used to be pre-sized to it
|
||||
/// ("Array dimensions exceeded") — every uncapped Gateway events read failed and counted as a backend failure.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task An_uncapped_forward_events_read_is_served()
|
||||
{
|
||||
var fake = Store(30);
|
||||
var ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance);
|
||||
|
||||
var r = await ds.ReadEventsAsync(null, T0, T0.AddMinutes(1), int.MaxValue, Ct);
|
||||
|
||||
r.Events.Count.ShouldBe(30);
|
||||
r.ContinuationPoint.ShouldBeNull();
|
||||
ds.GetHealthSnapshot().TotalFailures.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_uncapped_reverse_events_read_is_one_forward_read_newest_first()
|
||||
{
|
||||
var fake = Store(30);
|
||||
var ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance);
|
||||
|
||||
var page = await ReverseEventsReader.ReadAsync(ds, null, hi: T0.AddSeconds(25), lo: T0.AddSeconds(5), want: 0,
|
||||
maxTieCluster: 65536, nowUtc: T0.AddHours(1), hint: default, Ct);
|
||||
|
||||
page.Events.Select(e => e.EventId).ShouldBe(Enumerable.Range(6, 20).Reverse().Select(i => $"e{i:0000}"));
|
||||
fake.ReadEventsCallCount.ShouldBe(1);
|
||||
ds.GetHealthSnapshot().TotalFailures.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_capped_reverse_events_read_walked_back_over_the_gateway_returns_the_latest_newest_first()
|
||||
{
|
||||
var fake = Store(300);
|
||||
var ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance);
|
||||
|
||||
var page = await ReverseEventsReader.ReadAsync(ds, null, hi: T0.AddSeconds(200), lo: T0.AddSeconds(50), want: 10,
|
||||
maxTieCluster: 65536, nowUtc: T0.AddHours(1), hint: default, Ct);
|
||||
|
||||
page.Events.Select(e => e.EventId).ShouldBe(Enumerable.Range(191, 10).Reverse().Select(i => $"e{i:0000}"));
|
||||
fake.LastReadEventsStartUtc.ShouldBeLessThanOrEqualTo(fake.LastReadEventsEndUtc);
|
||||
ds.GetHealthSnapshot().TotalFailures.ShouldBe(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user