HistoryRead-Raw with NumValuesPerNode=1 returns an empty page between every value #692
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by the #689–#691 live AVEVA gate (2026-10-13, master
942ca7bc). Affects every backend.Symptom: paging a raw read with
NumValuesPerNode = 1alternates a value page with an EMPTY page (GoodNoData+ continuation point). 8 values cost 17 pages and 25 backend reads; on Gateway each empty page asks for ~65,570 rows. Forward and reverse, with or withoutReturnBounds.Repro: raw read on
HistGW.LiveTest.Gate685over[19:00:00.1, 19:00:10.5)with N=1, paged to completion.Likely area: the #669 resume/tie-cluster logic in
OtOpcUaNodeManager.ServeRawPagedAsync(TrimBoundaryDuplicates+ the stall/over-fetch path) — with N=1 a resumed page re-reads the boundary sample, trims it, and returns nothing. Expected: one value per page. Pin with a paging test at N=1 over all three backend shapes and a read-count assertion.Also from the #691.1 re-review (low): the N=1 ReturnBounds path (
BoundedReadHasAnythingAsync) repeats the data/end-bound reads that page 2 does again, and drops the walk hint it just computed; andHistoryBounds.IsPlaceholdermatches on statusBadBoundNotFound+ null value, so a row a client stored with that status via HistoryUpdate would be taken for a placeholder (theoretical; a marker flag would close it).Evidence: scratchpad
livegate-aveva-689.md(F1).Fixed on master
66a2b0c3(mergeaa923cea; commits8dfa1651,3d7eea62,e04e1bf1). Live AVEVA gate PASS (2026-10-13).Root cause: a resumed raw page restarts at the boundary timestamp inclusive but asked the backend for only N rows;
TrimBoundaryDuplicatesthen dropped the ties already served, so every resumed page was short. At N=1 it came back empty and was mistaken for a stuck tie cluster, triggering theMaxTieClusterOverfetch + 1over-fetch. Every backend, both directions, with and without ReturnBounds.Fix: each page asks for
N + skip + 1rows (the ties it will trim, the page, one lookahead row). No lookahead row ⇒ last page, no continuation point. The over-fetch fires only for a genuine cluster at the boundary; a cursor inside one is markedWithinTieCluster.Live (Gate685
[19:00:00.1, 19:00:10.5), N=1): 8 pages / 8 reads / 151 rows requested (was 17 / 25 / 524,713). With bounds 10 / 10, forward and reverse. No empty page with a continuation point; largest request 1,042 rows.Also fixed:
Bad_BoundNotFound/null row is data.SliceTieClustercould resume AT the exclusive EndTime and return samples stamped there. Fixed.Client-visible: a full last page no longer carries a continuation point. Exception: an oversized tie cluster (more ties at one timestamp than N plus those already sent) can still give a short page with a continuation point, or one empty final page (documented in
docs/Historian.md).Tests: 85-case
NodeManagerHistoryReadPageFillTests. Paged-vs-unpaged fuzz: 0 mismatches over 60k cases (master: ~1 in 160).Follow-up: #697 (backend outage answered BadHistoryOperationUnsupported, seen during this gate).