Files
lmxopcua/HISTORIAN-GATEWAY-INTEGRATION-ISSUES.md
T
Joseph Doherty 384dbd7d36
v2-ci / build (push) Failing after 42s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs(claude): refresh CLAUDE.md + record historian-gateway integration/dev-rig state
CLAUDE.md: add scadaproj sister-project index, fix mxaccessgw path to
~/Desktop/MxAccessGateway, note the temporary CVE-2025-6965 NuGetAuditSuppress,
and correct the driver-typed tag-editor list (OpcUaClient is now mapped; only
Galaxy falls back). Also captures docker-dev data-plane GroupToRole + ServerHistorian
live-verify config and the HistorianGateway integration/backlog notes.
2026-07-07 11:46:02 -04:00

184 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HistorianGateway backend — issues identified
**Date:** 2026-06-27
**Context:** Installed the `ZB.MOM.WW.HistorianGateway` sidecar on **wonder-app-vd03** and updated OtOpcUa to
`origin/master` (`245316d8`, PR #423 "HistorianGateway as the OtOpcUa historian backend"), then smoke-tested
the OtOpcUa→gateway link live. These are the issues found during that work.
> **File/line references are against `origin/master` @ `245316d8`** (the build that contains the gateway
> backend — `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/`). They will not resolve on older
> local checkouts that predate PR #423.
> **Live evidence** came from the gateway's Prometheus `/metrics` on the box: an OPC UA `historyread` on a
> historized tag produced `historian_gateway_grpc_requests_total{method="ReadRaw",service="HistorianRead"}=1`
> and `historian_session_pool_warm_sessions=1`, while a historized-tag **deploy** produced **zero**
> `EnsureTags` calls.
---
## Issue 1 — `GatewayTagProvisioner` is never wired; EnsureTags auto-provisioning is dormant (HIGH, confirmed)
**Summary.** PR #423 ships `GatewayTagProvisioner` (the `IHistorianProvisioning` implementation that calls the
gateway's `EnsureTags`) and unit tests for it, but it is **never registered in DI and never passed into the
`AddressSpaceApplier`**. At runtime the applier uses the no-op `NullHistorianProvisioning`, so **deploying
historized tags does NOT provision them in the historian**. The documented "EnsureTags auto-provisions
historian tags on deploy" behaviour does not occur.
**Root cause (code).**
- `AddressSpaceApplier` constructor coalesces a missing provisioner to the no-op:
- `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceApplier.cs:63`
`_provisioning = provisioning ?? NullHistorianProvisioning.Instance;`
- The **only** construction site omits the `provisioning:` argument:
- `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs:258`
```csharp
var applier = new AddressSpaceApplier(
addressSpaceSink,
loggerFactory.CreateLogger<AddressSpaceApplier>(),
historizedSubscriptions: historizedSubscriptions); // <-- no provisioning:
```
- **No `IHistorianProvisioning` / `GatewayTagProvisioner` registration exists anywhere** in the repo
(`grep` over `origin/master` finds only the interface definition, `NullHistorianProvisioning`, and the
`GatewayTagProvisioner` constructor definition itself — no `AddSingleton<IHistorianProvisioning, …>` and
no `new GatewayTagProvisioner(…)`).
- Consequently `ProvisionHistorizedTags` (called unconditionally at
`AddressSpaceApplier.cs:208`, body at `:226`, dispatch at `:261`) builds the request list and invokes
`_provisioning.EnsureTagsAsync(...)` on the **no-op**, which returns without contacting the gateway.
**Live evidence.** On wonder-app-vd03: flagged the `parts-count` equipment tag `isHistorized:true`, deployed,
and restarted the host. The gateway `/metrics` showed **no** gRPC request series for `HistorianTags`/`EnsureTags`
(zero gateway calls). By contrast, an explicit OPC UA `historyread` on the same tag *did* reach the gateway
(`ReadRaw`), confirming the read path is wired but the provisioning path is not.
**Impact.**
- Historized tags are never created in the historian on deploy.
- When **continuous historization** (`ContinuousHistorization:Enabled=true`) is later turned on, the recorder
drains live values to historian tags that were never provisioned — those writes will fail / land nowhere
unless the tags are created by some other means.
- Docs (`docs/Historian.md`, repo `CLAUDE.md`, and the umbrella scadaproj `CLAUDE.md`) describe provisioning
as active; that claim is currently false. See Issue 4.
**Recommended fix (two parts).**
1. **Register the provisioner** in the Host alongside the existing gateway wiring — in
`src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs`, inside the `if (serverHistorianOptions.Enabled)` block that
already calls `AddServerHistorian` / `AddAlarmHistorian` — e.g.
```csharp
builder.Services.AddSingleton<IHistorianProvisioning>(sp =>
new GatewayTagProvisioner(
HistorianGatewayClientAdapter.Create(serverHistorianOptions, sp.GetRequiredService<ILoggerFactory>()),
sp.GetRequiredService<ILogger<GatewayTagProvisioner>>()));
```
(mirror the `GatewayHistorianValueWriter` registration pattern already in that block).
2. **Pass it into the applier** at `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs:258`
by resolving it the same way sibling deps are resolved there (`resolver.GetService<…>()`):
```csharp
var provisioning = resolver.GetService<IHistorianProvisioning>();
var applier = new AddressSpaceApplier(
addressSpaceSink,
loggerFactory.CreateLogger<AddressSpaceApplier>(),
provisioning: provisioning,
historizedSubscriptions: historizedSubscriptions);
```
3. **Add a wiring/integration test** asserting `EnsureTags` is invoked on a historized-tag deploy. The existing
`GatewayTagProvisioner` unit tests pass but do not cover the DI wiring, which is exactly the gap that let
this ship dormant.
---
## Issue 2 — Provisioning success is logged nothing; the dormant path is invisible (LOW, observability)
**Summary.** The provisioning continuation in `AddressSpaceApplier` only emits a log line when
`result.Failed > 0 || result.Skipped > 0`. A fully-successful provisioning (`Ensured>0, Failed=0, Skipped=0`)
**and** the no-op `NullHistorianProvisioning` path both log **nothing**. This is what made Issue 1 invisible:
there was no log line either way, so "no provisioning log" could not be distinguished from "provisioning ran
and fully succeeded."
**Location.** `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceApplier.cs` — the
`EnsureTagsAsync(...).ContinueWith(...)` block (~lines 261283); the Information log is gated behind
`if (result.Failed > 0 || result.Skipped > 0)`.
**Recommended fix.** Emit an Information tally on every dispatch (e.g. `requested/ensured/skipped/failed`),
or at minimum whenever `Ensured > 0`, so a successful provisioning is observable and a no-op is detectable.
---
## Issue 3 — HistoryRead on a historized-but-unprovisioned tag blocks for the full CallTimeout (~30s) (MEDIUM, needs investigation)
**Summary.** An OPC UA `historyread` on a tag that is historized but has no corresponding historian
tag/data returned only after ~30 s — the gateway's `ReadRaw` recorded
`historian_gateway_grpc_request_duration_seconds_sum ≈ 30.5` for the single call, matching the
`ServerHistorian:CallTimeout` default of `00:00:30`. The read did not fail fast on an unknown tag; it ran to
the timeout and then errored (`outcome="error"`).
**Notes / attribution.**
- The slow leg is the gateway→historian read of a non-existent tag; the OtOpcUa-facing symptom is a 30 s OPC UA
HistoryRead latency governed by `ServerHistorianOptions.CallTimeout` (default 30 s, `…Runtime/Historian/ServerHistorianOptions.cs`).
- This is **coupled to Issue 1**: because provisioning never runs, freshly-historized tags don't exist in the
historian, so every HistoryRead on them hits this slow path until tags/data exist.
- Single data point (one unprovisioned tag); confirm whether the slow path is the historian, the gateway, or the
client-side timeout before changing behaviour.
**Recommended action.** Primarily fix Issue 1 so historian tags exist. Separately, consider a shorter default
HistoryRead timeout and/or fast-fail when the historian reports an unknown tag, so a missing tag doesn't hang an
OPC UA HistoryRead for 30 s.
---
## Issue 4 — Docs describe provisioning as active (LOW, documentation)
`docs/Historian.md`, the repo `CLAUDE.md`, and the umbrella `scadaproj/CLAUDE.md` state that the gateway backend
"auto-provisions historian tags on deploy (EnsureTags)". Given Issue 1, that is inaccurate in the shipped build.
Update the docs once Issue 1 is fixed, or flag the provisioning behaviour as not-yet-wired in the interim.
---
## Issue 5 — Provisioning covers only *newly-added* tags; flagging an existing tag for historization never provisions it (MEDIUM, follow-up to the Issue 1 fix)
**Status:** found while live-verifying the Issue 1 fix (commit `257214f7`) on wonder-app-vd03.
**Summary.** The Issue 1 fix correctly wires `GatewayTagProvisioner` and is confirmed deployed (the new
`"...provisioning completed (dispatched…"` log literal is present in the deployed `OpcUaServer.dll`). However,
`ProvisionHistorizedTags` still iterates **only** `plan.AddedEquipmentTags`:
- `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceApplier.cs` (`ProvisionHistorizedTags`):
`foreach (var tag in plan.AddedEquipmentTags)` — `ChangedEquipmentTags` is **not** considered.
So when an operator flags an **existing** tag `isHistorized:true` and deploys, that tag is classified
**Changed** (its config changed; the tag already existed) and is **never provisioned**. This is compounded by
the bootstrap "restore served state" behaviour: a host restart re-applies the served address space
**incrementally**, so an existing tag comes back as *unchanged/changed*, never *Added* — i.e. a restart does
not provision it either.
**Live evidence.** Flagging `parts-count` (an existing tag) historized + deploy + restart produced **zero**
`EnsureTags` calls at the gateway (`/metrics` had no `HistorianTags/EnsureTags` series). The read path on the
same tag *did* reach the gateway (`HistorianRead/ReadRaw`), confirming the tag was historized and the gateway
link works — only provisioning was skipped. (A raw `dbo.Tag` INSERT of a brand-new historized tag did not
materialise either, because a raw insert bypasses the normal compose/validate path — so the clean way to
exercise the *added* path is to author a new historized tag via the AdminUI.)
**Impact.** The most common operator action — take an existing tag and turn on historization — does **not**
auto-provision the historian tag. Continuous historization for such a tag would then write to a tag that does
not exist. Only genuinely **new** historized tags get provisioned.
**Recommended fix.** Provision the union of added **and** newly-historized changed tags: also iterate
`plan.ChangedEquipmentTags` (or compute "tags whose `IsHistorized` transitioned false→true") and include them
in the `EnsureTags` request. `EnsureTags` is idempotent, so re-ensuring an already-present tag is safe. Add a
test that flips an existing tag to historized and asserts `EnsureTags` is invoked.
---
## Pre-existing observation (not introduced by this work)
- **Serilog log path is service-CWD-relative.** On wonder-app-vd03 the host writes to
`C:\Windows\System32\logs\otopcua-<date>.log` (because the service CWD is `System32`), not under the install
directory. This was noted in prior sessions; re-observed here. Not a gateway-integration issue, but it makes
on-box diagnosis awkward and is worth fixing (anchor the Serilog file sink to an absolute/content-root path).
---
## What is working (for context)
The OtOpcUa↔gateway **read and credential path is confirmed working end-to-end** on wonder-app-vd03: an OPC UA
HistoryRead routed through `GatewayHistorianDataSource` to the gateway over h2c, authenticated with a
peppered-HMAC API key (`historian:read` scope), and leased a real historian session (the gateway reached
wonder-sql-vd03). Only the **provisioning** half (Issue 1) is unwired.