fix(historian): #692 review — a drained tie cluster never resumes onto the exclusive EndTime
F1 (pre-existing): SliceTieCluster continued a forward read while next <= endUtc, but the forward window is end-exclusive; an oversized cluster one step below EndTime resumed AT EndTime, the resume became a start == end point read and returned the EndTime samples as data (microsecond: EndTime 8.1 us truncated to 8.0). Continue only while next is strictly before EndTime truncated to the backend resolution. Tests: A_tie_cluster_just_below_the_end_time_never_resumes_onto_the_end_time (native + microsecond; both fail on the old rule) and A_cursor_inside_a_tie_cluster_over_fetches_without_a_normal_read_first (pins the WithinTieCluster shortcut by per-page read count; fails with the shortcut disabled). Docs: the oversized-cluster exception to 'every page full / no lookahead => last' (F2), and the N + skip + 1 strict-ceiling edge note referencing #694 (F4).
This commit is contained in:
+62
@@ -146,6 +146,68 @@ public sealed class NodeManagerHistoryReadPageFillTests(BoundsServerFixture fx)
|
||||
pages.Select(p => p.Count).ShouldBe([2, 2, 2, 2]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #692 review F1: an oversized tie cluster one step below EndTime used to resume AT EndTime (the drain check was
|
||||
/// <c>next <= end</c>, but the forward window is end-exclusive), so the next read was a <c>start == end</c> point
|
||||
/// read that returned the samples AT EndTime as data. Paged must equal unpaged — neither holds the EndTime samples.
|
||||
/// The microsecond shape truncates EndTime (8.1 µs ⇒ 8.0 µs) before the same thing happens.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(Shape.Native, 1L, 8L)]
|
||||
[InlineData(Shape.Microsecond, 10L, 81L)]
|
||||
public async Task A_tie_cluster_just_below_the_end_time_never_resumes_onto_the_end_time(Shape shape, long unit, long endTicks)
|
||||
{
|
||||
var series = new List<DataValueSnapshot>();
|
||||
var v = 0;
|
||||
foreach (var (at, count) in new[] { (5L, 5), (6L, 2), (7L, 3), (8L, 3) })
|
||||
for (var i = 0; i < count; i++)
|
||||
series.Add(Sample(v++, T0.AddTicks(at * unit)));
|
||||
var source = Source(shape, series);
|
||||
var (start, end) = (T0.AddSeconds(-3), T0.AddTicks(endTicks));
|
||||
|
||||
var unpaged = await SinglePageAsync(source, start, end, bounds: false);
|
||||
unpaged.Count.ShouldBe(10, "the samples at the (exclusive) end time are not data");
|
||||
|
||||
var pages = await DrainPagesAsync(source, start, end, 1, bounds: false);
|
||||
Render(pages.SelectMany(p => p)).ShouldBe(Render(unpaged));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #692: a cursor left inside an oversized tie cluster is flagged, and its next page goes straight to the cluster
|
||||
/// over-fetch — one backend read — instead of first re-reading <c>N + skip + 1</c> rows it knows are ties.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_cursor_inside_a_tie_cluster_over_fetches_without_a_normal_read_first()
|
||||
{
|
||||
// a @1s, five ties @2s, b @3s; N = 1.
|
||||
var series = new List<DataValueSnapshot> { Sample(0, T0.AddSeconds(1)) };
|
||||
for (var i = 1; i <= 5; i++) series.Add(Sample(i, T0.AddSeconds(2)));
|
||||
series.Add(Sample(6, T0.AddSeconds(3)));
|
||||
var source = Source(Shape.Native, series);
|
||||
var (start, end) = (T0, T0.AddSeconds(10));
|
||||
|
||||
var readsPerPage = new List<int>();
|
||||
var all = new List<DataValue>();
|
||||
byte[]? cp = null;
|
||||
do
|
||||
{
|
||||
source.ResetCounts();
|
||||
var (values, error, next) = await ReadAsync(source, start, end, 1, bounds: false, cp);
|
||||
error.StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
values.Count.ShouldBe(1);
|
||||
all.AddRange(values);
|
||||
readsPerPage.Add(source.Reads);
|
||||
cp = next;
|
||||
readsPerPage.Count.ShouldBeLessThan(20);
|
||||
}
|
||||
while (cp is not null);
|
||||
|
||||
all.Select(x => (double)x.Value).ShouldBe([0d, 1d, 2d, 3d, 4d, 5d, 6d]);
|
||||
// Page 1 [a], page 2 [t1]: one read each. Page 3 [t2]: the read stalls on the cluster, then the over-fetch (2).
|
||||
// Pages 4 [t3], 5 [t4], 6 [t5]: the flagged cursor over-fetches directly (1 each). Page 7 [b]: one read.
|
||||
readsPerPage.ShouldBe([1, 1, 2, 1, 1, 1, 1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #692 (low): a one-value bounded read whose start bound is not found asks whether the read holds anything. When the
|
||||
/// answer is "only the end bound", the bound it just found rides the continuation point — the next page is served
|
||||
|
||||
Reference in New Issue
Block a user