docs(historian): close #491 — read-back and EnsureTags proven against wonder-sql-vd03
v2-ci / build (push) Successful in 4m11s
v2-ci / unit-tests (push) Failing after 14m17s

The 2026-08-01 run closed every leg except read-back, which no reachable historian could
serve that day (the LAN instance accepts writes but serves no reads under a licence tag-count
violation; the licensed one needed the VPN). With the VPN up the run was repeated against
wonder-sql-vd03 and the remaining legs pass:

- EnsureTags provisioning: ensured=2, failed=0 — the path the LAN box could not exercise at
  all (it is RemoteGrpc-only and that CAP was not listening).
- Read-back: two Modbus registers carrying DISTINCT NON-ZERO constants were historized, and
  each read back as its own value (HR20 -> 8224, HR3 -> 120, quality 192). A first attempt
  read back 0, which proves nothing — the same trap #536 recorded, so the proof was rebuilt
  on values a crossed or fabricated answer could not fake.

Found during the run and FILED rather than fixed:

- #538 — one permanently-failing tag re-writes every healthy tag on every drain pass, forever.
  DrainOnceAsync writes EVERY per-tag group and then acks all-or-nothing, so a tag whose
  historian tag does not exist stops the FIFO head advancing while the healthy tags' values
  are re-submitted every drain interval; the outbox never truncates (Capacity defaults to 0 =
  unbounded, observed at 8.3 MB). The all-or-nothing ack is deliberate — RemoveAsync truncates
  THROUGH an id, so partial acking could drop older unwritten entries — so the fix changes the
  outbox contract and does not belong in this commit.

Also re-confirmed live: EnsureTags provisioning fires only for newly-ADDED historized tags, so
flipping an existing tag's historianTagname provisions nothing (Issue 5 of
2026-06-27-historian-gateway-integration-issues.md).

No production code changes — the recorder fix shipped in ad744734.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
Joseph Doherty
2026-08-04 11:29:15 -04:00
parent ad74473452
commit e77ea4aa73
3 changed files with 114 additions and 26 deletions
+19 -11
View File
@@ -114,12 +114,17 @@ source it from there.
> hardcodes OPC-DA Good, `192`). Capturing real per-node quality will not persist until the gateway honors
> the field — see `GatewayHistorianValueWriter`'s remarks (archreview 06/C-7).
> ✅ **Continuous value capture is LIVE-PROVEN as far as the historian's door
> ([#491](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/491), gate run 2026-08-01 — see
> `docs/plans/2026-08-01-continuous-historization-live-gate.md`).** Against a real
> `ZB.MOM.WW.HistorianGateway` talking to a real AVEVA Historian: deploy → dependency-mux value delta →
> FasterLog outbox append → drain → `WriteLiveValues` **accepted**, and **restart convergence** (the
> recorder re-registers the deployed ref set after a process restart, with no new deployment) both pass.
> ✅ **Continuous value capture is LIVE-PROVEN END-TO-END
> ([#491](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/491) CLOSED — gate run 2026-08-01,
> completed 2026-08-04; see `docs/plans/2026-08-01-continuous-historization-live-gate.md`).** Against a
> real `ZB.MOM.WW.HistorianGateway` talking to the real AVEVA Historian on `wonder-sql-vd03`: deploy →
> `EnsureTags` provisioning → dependency-mux value delta → FasterLog outbox append → drain →
> `WriteLiveValues` → **value read back from the historian**, plus **restart convergence** (the recorder
> re-registers the deployed ref set after a process restart, with no new deployment).
>
> The read-back used two registers carrying **distinct non-zero** constants (`8224`, `120`), each of
> which read back as its own value — a `0`-valued source cannot distinguish a correct value from a
> wrong one.
>
> ⚠️ The gate found a **production-fatal defect**: the recorder threw `NotSupportedException` on the
> **first value it ever appended**, because it touched `Self` after an `await …ConfigureAwait(false)`,
@@ -128,12 +133,15 @@ source it from there.
> value. Fixed; pinned by a regression test whose outbox fake completes on a **pool** thread (a fake
> returning `ValueTask.CompletedTask`, or even `await Task.Yield()`, cannot reproduce it).
>
> ❌ **Still unproven: the read-back hop** — no available historian can serve it today (the LAN instance
> accepts writes but serves no reads under a licence tag-count violation; the licensed corporate one needs
> the VPN). `EnsureTags` provisioning is likewise unexercised there (`RemoteGrpc`-only). **#491 stays open
> for that hop alone.**
> ⚠️ **One permanently-failing tag re-writes every healthy tag, forever
> ([#538](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/538)).** `DrainOnceAsync` writes **every**
> per-tag group in a batch and then acks **all-or-nothing**, so a tag whose historian tag does not exist
> stops the FIFO head advancing while the healthy tags' values are re-submitted on every drain pass —
> duplicate samples at identical timestamps, and an outbox that never truncates (`Capacity` defaults to
> `0` = unbounded; observed at 8.3 MB). The all-or-nothing ack itself is deliberate.
>
> ⚠️ **Both nodes of a redundant pair historize the same value** — the recorder has no Primary gate,
> ⚠️ **Both nodes of a redundant pair historize the same value
> ([#537](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/537))** — the recorder has no Primary gate,
> unlike the alarm store-and-forward sink, so the historian receives duplicate live values per tag.
>
> **Related, separately open:** `EnsureTags` provisioning fires only for **newly-added** historized