feat(historian): page within oversized tie clusters (#400) instead of loud-failing
This commit is contained in:
@@ -150,4 +150,68 @@ internal static class HistoryPaging
|
||||
for (var j = i; j < resumedPage.Count; j++) trimmed.Add(resumedPage[j]);
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Page WITHIN a single oversized "tie cluster" — a set of raw samples that all share one
|
||||
/// SourceTimestamp <paramref name="boundaryT"/> and is larger than the client's per-page cap.
|
||||
/// <para>
|
||||
/// The fixed-<c>(start, end, cap)</c> historian backend cannot skip/offset, so an oversized
|
||||
/// tie cluster defeats the (timestamp, skip) resume cursor that <see cref="ComputeResumeCursor"/>
|
||||
/// builds: every resume re-reads the first <c>cap</c> ties, the boundary-tie trim empties the
|
||||
/// page, and the cursor never advances. To page past it, the caller over-fetches the WHOLE
|
||||
/// cluster (a bounded <c>start == end == T</c> read) and hands it here. We then carve out the
|
||||
/// next <paramref name="cap"/> ties starting at <paramref name="skip"/> and compute a cursor
|
||||
/// that advances within the cluster, then steps off it when it is drained.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Advance is lossless.</b> When the slice drains the cluster (<c>emitted == clusterCount</c>)
|
||||
/// we resume from <c>T + 1 tick</c> with a fresh skip of <c>0</c>. This skips no data because no
|
||||
/// <see cref="DateTime"/> value exists strictly between <c>T</c> and <c>T + 1 tick</c> — a tick
|
||||
/// (100 ns) is the type's resolution — so every remaining sample in the window has a timestamp
|
||||
/// of at least <c>T + 1 tick</c>. We do NOT resume inclusively at <c>T</c> here (the way
|
||||
/// <see cref="ComputeResumeCursor"/> does for cross-boundary ties): we have already over-fetched
|
||||
/// and emitted the entire <c>T</c> cluster, so resuming at <c>T</c> would needlessly re-read it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Short-page-with-CP exception.</b> A page that fully drains the cluster but is SHORTER than
|
||||
/// <paramref name="cap"/> (the cluster had fewer than <c>cap</c> remaining ties) STILL emits a
|
||||
/// continuation point when the window extends past <c>T</c> (<c>T + 1 tick <= endUtc</c>). This
|
||||
/// deliberately violates the usual "short page ⇒ terminal" rule (<see cref="IsFullPage"/>):
|
||||
/// the page is short only because the cluster ran out, not because the window did, so there may
|
||||
/// still be data after <c>T</c> that the next page must read.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Self-heal.</b> If <paramref name="skip"/> meets or exceeds <paramref name="clusterCount"/>
|
||||
/// (e.g. a stale cursor against a cluster that shrank between reads), <c>sliceStart</c> clamps to
|
||||
/// <paramref name="clusterCount"/> and <c>sliceCount</c> is <c>0</c>; since <c>emitted</c> then
|
||||
/// equals <paramref name="clusterCount"/>, the cursor advances/terminates rather than looping.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="clusterCount">The total number of ties at <paramref name="boundaryT"/> (the over-fetched cluster size).</param>
|
||||
/// <param name="skip">How many ties at <paramref name="boundaryT"/> were already emitted on prior pages.</param>
|
||||
/// <param name="cap">The client's per-page cap (<c>NumValuesPerNode</c>); must be > 0 to make progress.</param>
|
||||
/// <param name="boundaryT">The single SourceTimestamp every tie in the cluster shares.</param>
|
||||
/// <param name="endUtc">The (inclusive) upper bound of the read window; unchanged across pages.</param>
|
||||
/// <param name="sliceStart">The index into the cluster the emitted slice starts at (clamped to the count).</param>
|
||||
/// <param name="sliceCount">How many ties the emitted slice contains (may be <c>0</c> on a stale-skip self-heal).</param>
|
||||
/// <param name="nextStartUtc">The next page's inclusive start: <paramref name="boundaryT"/> while the
|
||||
/// cluster still has un-emitted ties, <c>boundaryT + 1 tick</c> once it is drained and the window
|
||||
/// remains, or <c>null</c> when the window is exhausted (terminal — no continuation point).</param>
|
||||
/// <param name="nextSkip">The next page's boundary skip count: the running emitted-tie total while the
|
||||
/// cluster drains, else <c>0</c> after advancing past it.</param>
|
||||
public static void SliceTieCluster(
|
||||
int clusterCount, int skip, uint cap, DateTime boundaryT, DateTime endUtc,
|
||||
out int sliceStart, out int sliceCount, out DateTime? nextStartUtc, out int nextSkip)
|
||||
{
|
||||
sliceStart = Math.Min(skip, clusterCount);
|
||||
sliceCount = Math.Min((int)cap, clusterCount - sliceStart);
|
||||
var emitted = sliceStart + sliceCount;
|
||||
if (emitted < clusterCount) { nextStartUtc = boundaryT; nextSkip = emitted; }
|
||||
else
|
||||
{
|
||||
var next = boundaryT.AddTicks(1);
|
||||
if (next <= endUtc) { nextStartUtc = next; nextSkip = 0; }
|
||||
else { nextStartUtc = null; nextSkip = 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user