diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs index 3b9c1b97..309a0b63 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs @@ -143,8 +143,8 @@ public interface IHistorianDataSource : IDisposable /// /// /// The default forwards a forward read to the five-argument overload and throws - /// for a reverse one — a backend that cannot read newest-first (Gateway, - /// Null). The server never relies on that throw: it consults first and walks + /// for a reverse one — a backend that cannot read newest-first (the + /// Gateway). The server never relies on that throw: it consults first and walks /// such a read back from forward reads (#691.4), never returning the earliest events in ascending order as if /// they were the answer. A backend that serves reverse natively must override BOTH members. /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs index 9d97b317..811e3dc2 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs @@ -61,13 +61,12 @@ public sealed class NullHistorianDataSource : IHistorianDataSource public bool SupportsReverseRaw => true; /// - /// false (Gitea #675): there is no history to read in either direction; the server walks a reverse - /// read back from (empty) forward reads like any backend that cannot read newest-first (#691.4). - public bool SupportsReverseEvents => false; + /// true, like (Gitea #691.4 review; false under #675): there is no + /// history to read in either direction — no need for the server to walk back through empty forward reads. + public bool SupportsReverseEvents => true; /// - /// Forward ⇒ the empty result; reverse ⇒ , the interface contract for a - /// backend without . + /// The empty result in either direction. public Task ReadEventsAsync( string? sourceName, DateTime startUtc, @@ -75,9 +74,7 @@ public sealed class NullHistorianDataSource : IHistorianDataSource int maxEvents, bool reverse, CancellationToken cancellationToken) => - reverse - ? throw new NotSupportedException("The null historian cannot read events newest-first (reverse).") - : Task.FromResult(EmptyEvents); + Task.FromResult(EmptyEvents); /// /// diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index 8488e171..a939ed3c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -5291,7 +5291,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 reverse = startUtc > endUtc; } - // A reverse read on a backend that cannot read newest-first (Gateway, Null) is never sent there (#675 refused it + // A reverse read on a backend that cannot read newest-first (the Gateway) is never sent there (#675 refused it // here); since #691.4 it is walked back from bounded forward reads — the raw walk (#686), with ties ordered by // EventId because the gateway orders by event time only. Decided by the capability, not by catching an // exception, so a NotSupportedException thrown from inside a backend stays a backend fault. diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ReverseEventsReader.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ReverseEventsReader.cs index 52ab3bfd..f19648d3 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ReverseEventsReader.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ReverseEventsReader.cs @@ -9,7 +9,7 @@ internal readonly record struct ReverseEventsPage(List Events, /// /// Gitea #691.4: serves a Part 11 reverse events read — (lo, hi] newest-first — on a backend that reads events -/// forward only ( = false: Gateway, Null) with the +/// forward only ( = false: the Gateway) with the /// . A segment read is cut short when the source flags truncation (its continuation point); ties /// are ordered by because the gateway orders by event time only. /// diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/Historian/NullHistorianDataSourceTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/Historian/NullHistorianDataSourceTests.cs index e9045e7b..4c4b1dd7 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/Historian/NullHistorianDataSourceTests.cs +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/Historian/NullHistorianDataSourceTests.cs @@ -68,17 +68,19 @@ public sealed class NullHistorianDataSourceTests } /// - /// #675: the direction-aware overload's default — forward is the five-argument read, reverse is refused - /// (the server answers BadHistoryOperationUnsupported). + /// #691.4 review: like raw, the null source reads events in either direction (empty) and says so — the server sends + /// it the reverse read directly instead of walking back through empty forward reads (it refused reverse under #675). /// [Fact] - public async Task Direction_aware_ReadEventsAsync_forwards_forward_and_refuses_reverse() + public async Task Direction_aware_ReadEventsAsync_is_empty_in_both_directions() { IHistorianDataSource source = Source; + source.SupportsReverseEvents.ShouldBeTrue(); (await source.ReadEventsAsync(null, DateTime.UtcNow.AddHours(-1), DateTime.UtcNow, 0, reverse: false, Ct)) .Events.ShouldBeEmpty(); - await Should.ThrowAsync(() => - source.ReadEventsAsync(null, DateTime.UtcNow, DateTime.UtcNow.AddHours(-1), 0, reverse: true, Ct)); + var reverse = await source.ReadEventsAsync(null, DateTime.UtcNow, DateTime.UtcNow.AddHours(-1), 0, reverse: true, Ct); + reverse.Events.ShouldBeEmpty(); + reverse.ContinuationPoint.ShouldBeNull(); } /// GetHealthSnapshot reports a fully-disabled (zeroed, disconnected, no-node) shape.