feat(historian): server-side continuation-point paging for HistoryRead-Raw
The Wonderware historian backend is single-shot — it returns up to NumValuesPerNode samples with a null continuation point — so paging is synthesised server-side, time-based, for the only count-capped arm (Raw): - A full page (count == NumValuesPerNode, NumValuesPerNode > 0) emits an opaque 16-byte continuation point and stores a resume cursor; a short page (or NumValuesPerNode == 0 "all values") emits none. - A resume read takes the stored cursor, reads the next page from the boundary forward, and emits a fresh CP only if that page is also full. - The resume cursor is tie-safe (HistoryPaging.ComputeResumeCursor / TrimBoundaryDuplicates): the next page resumes from the boundary timestamp INCLUSIVE and drops the head ties already returned, so samples sharing the boundary SourceTimestamp are neither duplicated nor skipped. Continuation points are bound to the OPC UA session via the SDK's ISession.SaveHistoryContinuationPoint / RestoreHistoryContinuationPoint store (SessionHistoryContinuationStore) — capped by ServerConfiguration. MaxHistoryContinuationPoints (default 100, oldest-evicted) and disposed on session close. releaseContinuationPoints is honoured via an override of HistoryReleaseContinuationPoints (the base dispatcher routes release-only reads there, never to the per-details arms). An unknown / evicted / released point resumes to BadContinuationPointInvalid. Processed and AtTime stay single-shot: neither details type carries a client count cap, so the single-shot backend returns the complete result in one read and there is no "full page" signal to page on (spec-conformant). Modified-value history remains out of scope. The pure paging decisions + CP store contract are unit-tested via HistoryPaging + InMemoryHistoryContinuationStore; the full multi-page round trip is driven end-to-end through the node manager with an in-memory store + a series-backed fake historian (the in-process harness is session-less).
This commit is contained in:
+28
-3
@@ -121,11 +121,36 @@ A historized node with no historian configured never returns an error status —
|
||||
empty. This means a deployment can author and publish historized tags before the historian
|
||||
sidecar is provisioned, without producing error spikes in connected clients.
|
||||
|
||||
### Continuation-point paging (Raw)
|
||||
|
||||
`HistoryRead-Raw` is paged server-side. The historian backend is single-shot (it returns up to
|
||||
`NumValuesPerNode` samples with no continuation point of its own), so the server synthesises
|
||||
paging time-based:
|
||||
|
||||
- A page that returns **exactly** `NumValuesPerNode` samples (with `NumValuesPerNode > 0`) MAY
|
||||
have more behind it, so the server stores a resume cursor and returns an opaque continuation
|
||||
point (16 bytes). The client hands it back to fetch the next page.
|
||||
- A short page (fewer than the cap) is the last page — no continuation point.
|
||||
- `NumValuesPerNode == 0` ("all values, no limit") is never paged; the whole window returns in one
|
||||
shot.
|
||||
- The resume cursor is **tie-safe**: the next page resumes from the last returned sample's
|
||||
SourceTimestamp *inclusive* and drops the boundary samples already emitted, so samples sharing
|
||||
the boundary timestamp are neither duplicated nor skipped.
|
||||
|
||||
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
|
||||
`BadContinuationPointInvalid`. `releaseContinuationPoints` drops the stored cursors without reading
|
||||
data.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **No server-managed continuation points.** Each HistoryRead call is single-shot. The server
|
||||
honors the client's `NumValuesPerNode` limit but does not issue continuation points for
|
||||
large result sets. Paging across multiple calls is a documented follow-up.
|
||||
- **Processed and AtTime are single-shot** (no continuation points). Unlike Raw, neither
|
||||
`ReadProcessedDetails` nor `ReadAtTimeDetails` carries a client count cap (`NumValuesPerNode`):
|
||||
the Processed bucket count is deterministic (window / interval) and AtTime returns exactly one
|
||||
sample per requested timestamp, so the single-shot backend returns the complete result in one
|
||||
read and there is no "full page ⇒ maybe more" signal to page on. Returning the full result with
|
||||
no continuation point is spec-conformant.
|
||||
- **No modified-value history** (`HistoryReadModified`). Requests for modified values return
|
||||
`BadHistoryOperationUnsupported`. This is a follow-up.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user