fix(historian): #691.4 re-review — never narrow a walk segment below the 1 ms request granularity
A tie cluster's truncated read left the width at one tick; once the gap above it was under two milliseconds nothing floored it, so segHi stepped down one tick per read: 30 rows at one ms 0.3 s below hi with a carried 1 s width took 10,006 identical reads (S8). (a) The top-anchored width is floored at the granularity, Min kept; (b) a segment no wider than the granularity is the unit that escalates to MaxTieClusterOverfetch and then refuses - per millisecond, exact for the ms-resolution Gateway (documented). Both are needed: the floor alone loops on crowded reads with a small tie budget. S8 is a repo test (4 shapes, reads <= 30, 0 repeated requests); random-data tests now measure the refusal unit as rows in any 1 ms window. Reviewer harness, 5,000 seeds x default/MSONLY/CROWD: worst page 70 reads (default/MSONLY), 63 (CROWD) at M=65536, p99.9 27-37; no page over 500 reads at any tie budget.
This commit is contained in:
+31
-2
@@ -731,9 +731,38 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
|
||||
var max = (uint)rng.Next(1, 12);
|
||||
var end = new DateTime(t + rng.Next(0, 3) * TimeSpan.TicksPerSecond, DateTimeKind.Utc);
|
||||
|
||||
var collected = await DrainAsync(nm, nodeId, DateTime.MinValue, end, max, maxPages: 2000);
|
||||
// The walk refuses more than MaxTieClusterOverfetch samples within ONE MILLISECOND (#691.4 re-review), so with
|
||||
// the small budget a dense sub-millisecond burst is a legitimate BadTooManyOperations — and nothing else is.
|
||||
var msMax = 0;
|
||||
for (int i = 0, j = 0; i < series.Count; i++)
|
||||
{
|
||||
while (series[i].SourceTimestampUtc!.Value.Ticks - series[j].SourceTimestampUtc!.Value.Ticks >= TimeSpan.TicksPerMillisecond) j++;
|
||||
msMax = Math.Max(msMax, i - j + 1);
|
||||
}
|
||||
|
||||
collected.ShouldBe(Enumerable.Range(0, series.Count).Reverse().Select(i => (double)i), $"seed {seed}, N = {max}");
|
||||
var collected = new List<double>();
|
||||
byte[]? cp = null;
|
||||
var pages = 0;
|
||||
var refused = false;
|
||||
do
|
||||
{
|
||||
var (values, error, next) = ReadRaw(nm, nodeId, DateTime.MinValue, end, max, inboundCp: cp);
|
||||
if (error.StatusCode.Code == StatusCodes.BadTooManyOperations && msMax > nm.MaxTieClusterOverfetch)
|
||||
{
|
||||
refused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
error.StatusCode.Code.ShouldBe(StatusCodes.Good, $"seed {seed}, N = {max}, ms max {msMax}");
|
||||
collected.AddRange(values);
|
||||
cp = next;
|
||||
(++pages).ShouldBeLessThan(2000, "paging must terminate, not loop");
|
||||
}
|
||||
while (cp is not null);
|
||||
|
||||
var expected = Enumerable.Range(0, series.Count).Reverse().Select(i => (double)i).ToList();
|
||||
if (refused) collected.ShouldBe(expected.Take(collected.Count), $"seed {seed}: what came before the refusal");
|
||||
else collected.ShouldBe(expected, $"seed {seed}, N = {max}");
|
||||
}
|
||||
|
||||
await host.DisposeAsync();
|
||||
|
||||
Reference in New Issue
Block a user