fix(historian): #691.4 review — search the gap above a truncated read by gallop then bisect, never below 1 ms
Bisecting the gap above a truncated read halved below the 1 ms request granularity (requests are ms-rounded, so it re-sent identical requests) and bisected the WHOLE gap even when the burst ended right above the row it saw: a 1,026-row burst ending 100 days ago took 46 reads. After an empty top-anchored segment the walk now gallops up from the known row ((known + d, top], d x4, following each truncated read), then bisects the bracket, with halving floored at the granularity (the Min kept; a crowded gallop falls back to top-anchored halving). Burst: 18 reads, 0 repeated requests; dense tag stopped 2 days ago: 26 (gallop alone crawled: 94). Walk fuzzed at 4,000 seeds locally (400 in-tree). Docs: measured counts; CLAUDE.md gateway prerequisites list RuntimeDb:EventReadMaxRows.
This commit is contained in:
@@ -120,7 +120,7 @@ public sealed class ReverseWalkTests
|
||||
/// <summary>
|
||||
/// #689: a dense tag that stopped long ago. The walk's ×4 growth across the empty stretch overshoots into far more
|
||||
/// rows than one read takes; the truncated read only shows the OLDEST of them, so the walk must not keep re-entering
|
||||
/// that stretch — it bisects the gap above the newest row it saw.
|
||||
/// that stretch — it searches the gap above the newest row it saw (gallop up, then bisect). Measured: 26 reads.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_dense_tag_below_a_long_empty_stretch_is_found_in_a_bounded_number_of_reads()
|
||||
@@ -133,8 +133,8 @@ public sealed class ReverseWalkTests
|
||||
var page = await ReverseRawReader.ReadAsync(source, "T", T0, DateTime.MinValue, 3, 65536, T0, default(ReverseWalkHint), Ct);
|
||||
|
||||
page.Samples.Select(s => (double)s.Value!).ShouldBe([107_999d, 107_998d, 107_997d]);
|
||||
source.Reads.ShouldBeLessThanOrEqualTo(25);
|
||||
source.RowsReturned.ShouldBeLessThanOrEqualTo(25 * (ReverseWalk.DiscoveryRows + 2));
|
||||
source.Reads.ShouldBeLessThanOrEqualTo(30);
|
||||
source.RowsReturned.ShouldBeLessThanOrEqualTo(30 * (ReverseWalk.DiscoveryRows + 2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -185,6 +185,27 @@ public sealed class ReverseWalkTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #691.4 review: a burst just over the discovery read, sub-millisecond spaced, ending 100 days ago. The search for
|
||||
/// the top of the burst above the truncated read never re-sends an identical request (halving below the 1 ms request
|
||||
/// granularity did), and gallops up from the truncated read's newest row instead of bisecting the whole 100-day gap
|
||||
/// down to the millisecond (was 46 reads).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Bisecting_above_a_truncated_read_never_resends_an_identical_request()
|
||||
{
|
||||
var end = T0.AddDays(-100);
|
||||
var count = ReverseWalk.DiscoveryRows + 2;
|
||||
var series = Enumerable.Range(0, count).Select(i => At(end.Ticks - (count - 1 - i) * 1_000, i)).ToList(); // 0.1 ms apart
|
||||
var source = new AvevaShapedSource(series);
|
||||
|
||||
var page = await ReverseRawReader.ReadAsync(source, "T", T0, DateTime.MinValue, 3, 65536, T0, default(ReverseWalkHint), Ct);
|
||||
|
||||
page.Samples.Select(s => (double)s.Value!).ShouldBe([count - 1d, count - 2d, count - 3d]);
|
||||
source.RepeatedRequests.ShouldBe(0, string.Join("\n", source.Log));
|
||||
source.Reads.ShouldBeLessThanOrEqualTo(22, string.Join("\n", source.Log));
|
||||
}
|
||||
|
||||
private static long Ts(DataValueSnapshot s) => s.SourceTimestampUtc!.Value.Ticks;
|
||||
|
||||
/// <summary>Random ascending series: bursts, gaps, sub-millisecond stamps and tie clusters.</summary>
|
||||
@@ -221,7 +242,15 @@ public sealed class ReverseWalkTests
|
||||
public int Reads { get; private set; }
|
||||
public int BadRequests { get; private set; }
|
||||
|
||||
public void Reset() { MaxCap = 0; RowsReturned = 0; Reads = 0; }
|
||||
/// <summary>Requests that repeated an earlier request exactly (start, end, cap) — read-only, so pure waste.</summary>
|
||||
public int RepeatedRequests { get; private set; }
|
||||
|
||||
private readonly HashSet<(DateTime, DateTime, uint)> _seen = [];
|
||||
|
||||
/// <summary>Every request, for failure messages.</summary>
|
||||
public List<string> Log { get; } = [];
|
||||
|
||||
public void Reset() { MaxCap = 0; RowsReturned = 0; Reads = 0; RepeatedRequests = 0; _seen.Clear(); Log.Clear(); }
|
||||
|
||||
public bool SupportsReverseRaw => false;
|
||||
|
||||
@@ -229,6 +258,9 @@ public sealed class ReverseWalkTests
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Reads++;
|
||||
if (!_seen.Add((startUtc, endUtc, maxValuesPerNode))) RepeatedRequests++;
|
||||
|
||||
Log.Add($"[{startUtc:O} .. {endUtc:O}] cap {maxValuesPerNode}");
|
||||
if (startUtc > endUtc || maxValuesPerNode == 0 || startUtc < DateTime.UnixEpoch) BadRequests++;
|
||||
MaxCap = Math.Max(MaxCap, maxValuesPerNode);
|
||||
var stamp = roundUp && startUtc.Ticks % TimeSpan.TicksPerMillisecond != 0
|
||||
|
||||
Reference in New Issue
Block a user