fix(historian): the recorder died on its first value — live gate for continuous historization (#491)
Ran #491's live gate against a real ZB.MOM.WW.HistorianGateway talking to a real AVEVA Historian, and it found a production-fatal defect on the first value it ever saw. ContinuousHistorizationRecorder.OnValueChangedAsync touched `Self` after awaiting IHistorizationOutbox.AppendAsync(...).ConfigureAwait(false). ConfigureAwait(false) discards Akka's ActorTaskScheduler synchronization context, so `Self` — which reads ActorBase.Context — threw NotSupportedException. The throw escaped into supervision, which RESTARTED the actor; because production seeds an EMPTY ref set (the deployed set arrives later via UpdateHistorizedRefs), the restart silently discarded every historized ref. Net effect: one value captured, then nothing, with no further error to show for it. Fixed by capturing `IActorRef self = Self` before the await — the idiom the GetStatus handler already applied to Sender. Thirteen unit tests missed it because InMemoryOutbox.AppendAsync returned ValueTask.CompletedTask: a completed ValueTask never yields, so the continuation ran inline with the context still installed. The production FasterLog outbox fsyncs per entry and does yield. The first regression test was itself vacuous — `await Task.Yield()` posts back to the CAPTURED context, so the append still completed on the actor thread and reverting the fix left the test green. The fake must complete on a POOL thread (Task.Delay(1).ConfigureAwait(false)), as a real fsync does; with that, reverting the fix reddens exactly one test. The rig could not run this gate at all: docker-dev carried ServerHistorian__* but no ContinuousHistorization__* keys, so the recorder could never spawn. Both centrals now carry them (default OFF — the recorder needs ContinuousHistorization:Enabled AND ServerHistorian:Enabled), with the outbox on the per-node named volume so it survives a container recreate. ServerHistorian__UseTls was hard-coded true and is now parameterized; a plaintext h2c gateway was unreachable without it. Gate results: outbox append, drain, WriteLiveValues accepted by a real historian, and restart convergence (capture resumes after a restart with no new deployment) all pass. The read-back hop is NOT proven — the LAN historian accepts writes but serves no reads under a "License violation: Database tag count exceeds license tag count", and the licensed corporate one needs the VPN. EnsureTags provisioning is likewise unexercised there (RemoteGrpc-only, and that CAP is not listening). #491 stays open for the read-back hop alone. Also recorded, not fixed: the recorder has NO Primary gate, unlike the alarm store-and-forward sink, so both nodes of a redundant pair send duplicate live values. Gating it would introduce a failover window with no capture — that is a design decision, not a cleanup. Evidence: docs/plans/2026-08-01-continuous-historization-live-gate.md Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
@@ -374,8 +374,23 @@ services:
|
||||
ServerHistorian__Enabled: "${OTOPCUA_HISTORIAN_ENABLED:-false}"
|
||||
ServerHistorian__Endpoint: "${OTOPCUA_HISTORIAN_ENDPOINT:-}"
|
||||
ServerHistorian__ApiKey: "${ServerHistorian__ApiKey:-}"
|
||||
ServerHistorian__UseTls: "true"
|
||||
# UseTls must match the Endpoint scheme (http:// = h2c ⇒ false). Parameterized because a
|
||||
# locally-run gateway is usually plaintext h2c while a deployed one is https.
|
||||
ServerHistorian__UseTls: "${OTOPCUA_HISTORIAN_USETLS:-true}"
|
||||
ServerHistorian__AllowUntrustedServerCertificate: "true"
|
||||
# Continuous historization of driver tag values (Gitea #491's live gate). Gated on BOTH
|
||||
# ContinuousHistorization:Enabled AND ServerHistorian:Enabled — with either off no recorder is
|
||||
# spawned, so this stays inert on the default rig. OutboxPath is the FasterLog crash boundary and
|
||||
# points at the per-node named volume so the outbox survives a container recreate (that durability
|
||||
# is exactly what the restart-convergence leg of the gate exercises).
|
||||
# To run the gate:
|
||||
# OTOPCUA_HISTORIAN_ENABLED=true OTOPCUA_HISTORIAN_ENDPOINT=http://<gw-host>:5221 \
|
||||
# OTOPCUA_HISTORIAN_USETLS=false OTOPCUA_CONTHIST_ENABLED=true \
|
||||
# docker compose -f docker-dev/docker-compose.yml up -d central-1 central-2
|
||||
ContinuousHistorization__Enabled: "${OTOPCUA_CONTHIST_ENABLED:-false}"
|
||||
ContinuousHistorization__OutboxPath: "/app/data/historization-outbox"
|
||||
ContinuousHistorization__CommitMode: "${OTOPCUA_CONTHIST_COMMITMODE:-PerEntry}"
|
||||
ContinuousHistorization__DrainIntervalSeconds: "${OTOPCUA_CONTHIST_DRAIN_SECONDS:-2}"
|
||||
# Pin EF Core + ASP.NET Core to Warning so the per-poll Deployment SELECT /
|
||||
# "Executed DbCommand" Information|Debug lines stop flooding the Serilog
|
||||
# pipeline and starving the Akka cluster heartbeat thread. The host logs via
|
||||
@@ -478,8 +493,15 @@ services:
|
||||
ServerHistorian__Enabled: "${OTOPCUA_HISTORIAN_ENABLED:-false}"
|
||||
ServerHistorian__Endpoint: "${OTOPCUA_HISTORIAN_ENDPOINT:-}"
|
||||
ServerHistorian__ApiKey: "${ServerHistorian__ApiKey:-}"
|
||||
ServerHistorian__UseTls: "true"
|
||||
ServerHistorian__UseTls: "${OTOPCUA_HISTORIAN_USETLS:-true}"
|
||||
ServerHistorian__AllowUntrustedServerCertificate: "true"
|
||||
# Continuous historization — see central-1 for the full note + the gate recipe. Both centrals
|
||||
# carry it so the pair is symmetric; :9200 round-robins them, and the recorder must exist on
|
||||
# whichever node holds the drivers.
|
||||
ContinuousHistorization__Enabled: "${OTOPCUA_CONTHIST_ENABLED:-false}"
|
||||
ContinuousHistorization__OutboxPath: "/app/data/historization-outbox"
|
||||
ContinuousHistorization__CommitMode: "${OTOPCUA_CONTHIST_COMMITMODE:-PerEntry}"
|
||||
ContinuousHistorization__DrainIntervalSeconds: "${OTOPCUA_CONTHIST_DRAIN_SECONDS:-2}"
|
||||
# Quiet EF/AspNetCore SQL flood — see central-1 (Serilog override). mem_limit/
|
||||
# mem_reservation are inherited from the *otopcua-host anchor.
|
||||
Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore: "Warning"
|
||||
|
||||
Reference in New Issue
Block a user