fix(historian): address code review on Raw HistoryRead paging

C1 (critical): a boundary tie cluster larger than NumValuesPerNode could
silently truncate a resumed read to GoodNoData, permanently dropping the
un-emitted ties — the (timestamp, skip) cursor cannot advance past a single
timestamp the fixed-(start,end,cap) backend keeps re-returning. Now detected
and failed LOUDLY per node with BadHistoryOperationUnsupported + a log naming
the tag/timestamp/cap; documented in Historian.md with the larger-cap remedy.
Regression test Raw_tie_cluster_larger_than_page_fails_loudly_not_silently.

I3: build HistoryData before Save() so a projection failure can never orphan a
stored continuation cursor.

N1 (YAGNI): drop the never-produced HistoryReadKind enum + Processed-only
Aggregate/IntervalTicks fields from HistoryContinuationState — only Raw pages.

N3: ComputeResumeCursor guards its documented non-empty precondition.

I1: document InMemoryHistoryContinuationStore's eventual-consistency (test double).

Build clean, 182/182 OpcUaServer tests pass.
This commit is contained in:
Joseph Doherty
2026-06-15 05:15:07 -04:00
parent 94c3ca60fc
commit bea0b482d4
6 changed files with 101 additions and 36 deletions
+11
View File
@@ -137,6 +137,17 @@ paging time-based:
SourceTimestamp *inclusive* and drops the boundary samples already emitted, so samples sharing
the boundary timestamp are neither duplicated nor skipped.
> **Paging limitation — oversized tie clusters.** The tie-safe cursor is a `(timestamp, skip)`
> pair, and the single-shot backend only accepts `(start, end, cap)` — it cannot skip. So if **more
> samples share one `SourceTimestamp` than `NumValuesPerNode`** (a tie cluster larger than the page
> cap), the cursor cannot advance past that timestamp: every resume re-reads the same first `cap`
> ties. Rather than silently truncate the read to `GoodNoData` (which would permanently drop the
> un-emitted ties), the resume read fails that node **loudly** with
> `BadHistoryOperationUnsupported` and logs the tag + timestamp + cap. The operator's remedy is to
> re-issue the read with a larger `NumValuesPerNode`. For a single tag's raw history this is a data
> anomaly (raw samples normally carry strictly increasing distinct timestamps); a fully cursor-based
> fix that pages *within* a single timestamp is a possible follow-up.
Continuation points are bound to the OPC UA session (the SDK's
`ServerConfiguration.MaxHistoryContinuationPoints` cap, default 100, with oldest-eviction; points
are disposed when the session closes). Resuming an unknown / evicted / released point returns