fix(historian): #675 review follow-ups — SupportsReverseEvents capability, no catch-as-unsupported
- IHistorianDataSource.SupportsReverseEvents (default false; SqlServer/QuestDB true; Gateway/Null declare false). The node manager checks it BEFORE calling the source: an end-only capped events read becomes reverse only on a supporting backend (Gateway, the default, keeps the pre-#675 forward [MinValue, EndTime) paging), and an explicit reverse read on a non-supporting backend is BadHistoryOperationUnsupported without a call. - Removed the broad catch (NotSupportedException) in the events path: a NotSupportedException from inside a backend is now logged as a backend fault (Error), not an 'unsupported' Warning. - Null declares the reverse overload itself; guard test: every production IHistorianDataSource declares both the reverse overload and the capability (positive-control scan of all four backends). - Conformance: capability reported + oversized reverse tie cluster (forward over-fetch reversed == reverse read order; capped reverse read stopping inside the cluster flags truncation) on SqlServer + QuestDB. - docs/Historian.md: end-only capped events read is backward on SqlServer/QuestDB, forward on Gateway.
This commit is contained in:
@@ -96,6 +96,42 @@ public abstract class EventReadConformance : ConformanceBase
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_backend_reports_reverse_support()
|
||||
{
|
||||
using var src = B.CreateDataSource();
|
||||
src.SupportsReverseEvents.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_oversized_reverse_tie_cluster_sliced_the_way_the_server_does_matches_the_reverse_read_order()
|
||||
{
|
||||
// The server pages WITHIN a tie cluster larger than the page by over-fetching it FORWARD over [T, T + resolution)
|
||||
// and reversing it for a reverse read (OtOpcUaNodeManager.ServeEventsAsync + HistoryPaging.SliceTieCluster). That is
|
||||
// only correct if this backend's forward cluster, reversed, is exactly the order its reverse read emits the ties in
|
||||
// — the tie-breaker must be the same key in both directions. Drive both through the real backend.
|
||||
await B.SeedEventsAsync(Enumerable.Range(0, 12).Select(i => (At(2), $"c{i:00}", "s")).Append((At(3), "z", "s")).ToArray());
|
||||
using var src = B.CreateDataSource();
|
||||
|
||||
var reverseOrder = (await src.ReadEventsAsync("s", At(10), At(0), 0, reverse: true, Ct)).Events
|
||||
.Where(e => e.EventTimeUtc == B.Round(At(2))).Select(e => e.EventId).ToList();
|
||||
var clusterReversed = (await src.ReadEventsAsync("s", B.Round(At(2)), B.Round(At(2)).AddTicks(B.ResolutionTicks), 1000, reverse: false, Ct))
|
||||
.Events.Select(e => e.EventId).Reverse().ToList();
|
||||
|
||||
clusterReversed.Count.ShouldBe(12);
|
||||
clusterReversed.ShouldBe(reverseOrder);
|
||||
|
||||
// And slicing it at a cap of 5 (skip 0, 5, 10) walks the cluster newest-first with no duplicate or gap.
|
||||
var slices = new List<string>();
|
||||
for (var skip = 0; skip < clusterReversed.Count; skip += 5) slices.AddRange(clusterReversed.Skip(skip).Take(5));
|
||||
slices.ShouldBe(Enumerable.Range(0, 12).Reverse().Select(i => $"c{i:00}"));
|
||||
|
||||
// A capped reverse read stopping inside the cluster flags truncation, so the server's cluster branch is reachable.
|
||||
var capped = await src.ReadEventsAsync("s", At(10), At(0), 4, reverse: true, Ct);
|
||||
capped.Events.Select(e => e.EventId).ShouldBe(["z", "c11", "c10", "c09"]);
|
||||
capped.ContinuationPoint.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reverse_read_with_a_null_source_spans_every_source()
|
||||
{
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ public sealed class GatewayEventBoundsTests
|
||||
{
|
||||
var fake = new FakeHistorianGatewayClient();
|
||||
IHistorianDataSource ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance);
|
||||
ds.SupportsReverseEvents.ShouldBeFalse("the server must keep end-only reads forward on the default backend");
|
||||
|
||||
await Should.ThrowAsync<NotSupportedException>(() =>
|
||||
ds.ReadEventsAsync("Pump1", DateTime.UtcNow, DateTime.UtcNow.AddHours(-1), 10, reverse: true, Ct));
|
||||
|
||||
Reference in New Issue
Block a user