fix(historian): #691.3 review — remember a strict gateway's ceiling per data source; re-probe once when a bounded read comes back full
This commit is contained in:
+5
-2
@@ -100,8 +100,11 @@ public sealed class FakeHistorianGatewayClient : IHistorianGatewayClient
|
||||
LastReadRawStartUtc = startUtc;
|
||||
LastReadRawEndUtc = endUtc;
|
||||
LastReadRawMaxValues = maxValues;
|
||||
ReadRawCallCount++;
|
||||
RawCalls.Add((startUtc, endUtc, maxValues));
|
||||
lock (RawCalls)
|
||||
{
|
||||
ReadRawCallCount++;
|
||||
RawCalls.Add((startUtc, endUtc, maxValues));
|
||||
}
|
||||
if (MaxValuesCeiling is { } ceiling && maxValues > ceiling)
|
||||
{
|
||||
// The real gateway's refusal (HistorianReadService.ReadRaw), raised on the first MoveNext before any row, as
|
||||
|
||||
+86
@@ -110,6 +110,92 @@ public sealed class GatewayStrictCeilingTests
|
||||
fake.RawCalls.Select(c => c.MaxValues).ShouldBe([int.MaxValue, RawAggregateReader.GatewayDefaultMaxValuesCeiling]);
|
||||
}
|
||||
|
||||
// ---- the learned ceiling (review: a refused attempt counts as an error in the gateway's own metrics) -----------------
|
||||
|
||||
private static int Refusals(FakeHistorianGatewayClient fake) =>
|
||||
fake.RawCalls.Count(c => fake.MaxValuesCeiling is { } n && c.MaxValues > n);
|
||||
|
||||
[Fact]
|
||||
public async Task The_ceiling_is_learned_once_per_data_source_so_later_reads_are_not_refused()
|
||||
{
|
||||
var fake = Store(10, ceiling: 1000);
|
||||
var ds = Source(fake);
|
||||
await ds.ReadRawAsync("T", At(0), At(60), 0, Ct);
|
||||
await ds.ReadRawAsync("T", At(0), At(60), 0, Ct);
|
||||
await ds.ReadRawAsync("T", At(0), At(60), 995, Ct);
|
||||
await ds.ReadProcessedAsync("T", At(0), At(10), TimeSpan.FromSeconds(10), HistoryAggregateType.Count, Ct);
|
||||
|
||||
fake.RawCalls.Select(c => c.MaxValues).Take(4).ShouldBe([int.MaxValue, 1000, 1000, 1000]);
|
||||
fake.RawCalls[4].MaxValues.ShouldBe(1000); // the per-sample read shares it (its own budget 1,000,000)
|
||||
Refusals(fake).ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_gateway_that_now_streams_is_re_probed_when_a_learned_ceiling_read_comes_back_full()
|
||||
{
|
||||
var fake = Store(100, ceiling: 50);
|
||||
var ds = Source(fake);
|
||||
await ds.ReadRawAsync("T", At(0), At(5), 0, Ct); // learns 50
|
||||
fake.MaxValuesCeiling = null; // the operator enabled AllowStreamingBeyondCeiling
|
||||
|
||||
var r = await ds.ReadRawAsync("T", At(0), At(200), 0, Ct);
|
||||
r.Samples.Count.ShouldBe(100); // not BadTooManyOperations from a stale ceiling
|
||||
fake.RawCalls.Skip(2).Select(c => c.MaxValues).ShouldBe([50, int.MaxValue]);
|
||||
|
||||
await ds.ReadRawAsync("T", At(0), At(5), 0, Ct);
|
||||
fake.RawCalls[^1].MaxValues.ShouldBe(int.MaxValue); // forgotten
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_raised_but_still_strict_ceiling_is_re_learned_from_the_re_probe()
|
||||
{
|
||||
var fake = Store(100, ceiling: 50);
|
||||
var ds = Source(fake);
|
||||
await ds.ReadRawAsync("T", At(0), At(5), 0, Ct);
|
||||
fake.MaxValuesCeiling = 500;
|
||||
|
||||
(await ds.ReadRawAsync("T", At(0), At(200), 0, Ct)).Samples.Count.ShouldBe(100);
|
||||
fake.RawCalls.Skip(2).Select(c => c.MaxValues).ShouldBe([50, int.MaxValue, 500]);
|
||||
|
||||
await ds.ReadRawAsync("T", At(0), At(5), 0, Ct);
|
||||
fake.RawCalls[^1].MaxValues.ShouldBe(500);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_genuinely_over_ceiling_read_costs_one_re_probe_and_no_repeat_at_the_same_ceiling()
|
||||
{
|
||||
var fake = Store(100, ceiling: 50);
|
||||
var ds = Source(fake);
|
||||
await ds.ReadRawAsync("T", At(0), At(5), 0, Ct);
|
||||
await Should.ThrowAsync<HistoryReadLimitExceededException>(() => ds.ReadRawAsync("T", At(0), At(200), 0, Ct));
|
||||
fake.RawCalls.Skip(2).Select(c => c.MaxValues).ShouldBe([50, int.MaxValue]);
|
||||
ds.GetHealthSnapshot().TotalFailures.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_lowered_ceiling_is_re_learned_from_the_refusal_of_the_learned_one()
|
||||
{
|
||||
var fake = Store(10, ceiling: 1000);
|
||||
var ds = Source(fake);
|
||||
await ds.ReadRawAsync("T", At(0), At(60), 0, Ct);
|
||||
fake.MaxValuesCeiling = 50;
|
||||
|
||||
(await ds.ReadRawAsync("T", At(0), At(60), 0, Ct)).Samples.Count.ShouldBe(10);
|
||||
await ds.ReadRawAsync("T", At(0), At(60), 0, Ct);
|
||||
fake.RawCalls.Skip(2).Select(c => c.MaxValues).ShouldBe([1000, 50, 50]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Concurrent_reads_share_the_learned_ceiling_safely()
|
||||
{
|
||||
var fake = Store(10, ceiling: 1000);
|
||||
var ds = Source(fake);
|
||||
await ds.ReadRawAsync("T", At(0), At(60), 0, Ct);
|
||||
var results = await Task.WhenAll(Enumerable.Range(0, 32).Select(_ => Task.Run(() => ds.ReadRawAsync("T", At(0), At(60), 0, Ct))));
|
||||
results.ShouldAllBe(r => r.Samples.Count == 10);
|
||||
Refusals(fake).ShouldBe(1);
|
||||
}
|
||||
|
||||
// ---- raw, capped -----------------------------------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user