fix(historian-gateway): wire dormant GatewayTagProvisioner + provisioning observability/docs
PR #423 shipped GatewayTagProvisioner + unit tests but never registered it in DI nor passed it into the AddressSpaceApplier, so deploying historized tags used the no-op NullHistorianProvisioning and never called the gateway's EnsureTags (confirmed live on wonder-app-vd03: zero EnsureTags calls on a historized deploy). Addresses HISTORIAN-GATEWAY-INTEGRATION-ISSUES.md. Issue 1 (wire provisioner): - Runtime: AddHistorianProvisioning extension (gated on ServerHistorian:Enabled, mirrors AddServerHistorian) + NullHistorianProvisioning TryAdd default in AddOtOpcUaRuntime; WithOtOpcUaRuntimeActors resolves IHistorianProvisioning and passes it into the applier. - Gateway driver: GatewayHistorian.CreateProvisioner factory (mirrors CreateDataSource). - Host: Program.cs calls AddHistorianProvisioning after AddServerHistorian. - Tests: AddHistorianProvisioningTests (config-gated registration + the register->resolve->applier->EnsureTags chain). Issue 2 (observability): AddressSpaceApplier logs the provisioning tally on every successful dispatch (was gated behind Failed/Skipped > 0), including dispatched=N so a dispatched=N/requested=0 line flags the dormant no-op. +2 tests. Issue 3 (30s HistoryRead on unprovisioned tags): root coupling fixed by Issue 1; documented the CallTimeout knob + coupling. Default left at 30s pending the multi-data-point investigation the issue requests (lowering risks truncating legitimate large reads). Issue 4 (docs): docs/Historian.md gains a "Tag auto-provisioning (EnsureTags)" section and CLAUDE.md a wiring/gating note (both stress ServerHistorian:Enabled). Sibling scadaproj/CLAUDE.md carries no false claim -> unchanged. Pre-existing Serilog observation: anchor CWD to AppContext.BaseDirectory before AddZbSerilog so the relative file sink stops landing in C:\Windows\System32 under the Windows-service CWD. Builds 0-error; Runtime.Tests 355, OpcUaServer.Tests 329, Gateway.Tests 99 (+4 live-skipped) all green.
This commit is contained in:
+28
-1
@@ -82,7 +82,7 @@ and all HistoryRead calls on historized nodes return `GoodNoData` (empty, not an
|
||||
| `UseTls` | bool | `true` | Connect over TLS; must match the `Endpoint` scheme. |
|
||||
| `AllowUntrustedServerCertificate` | bool | `false` | Accept a self-signed / untrusted server certificate (dev / on-prem only). |
|
||||
| `CaCertificatePath` | string\|null | `null` | PEM CA file pinning the gateway's TLS chain. Null/empty uses the OS trust store. |
|
||||
| `CallTimeout` | TimeSpan | `00:00:30` | Per-call deadline applied to each unary gateway read. |
|
||||
| `CallTimeout` | TimeSpan | `00:00:30` | Per-call deadline applied to each unary gateway read. A read for a tag the historian does not yet know about — e.g. a freshly-historized tag that has **not** been provisioned (see [Tag auto-provisioning](#tag-auto-provisioning-ensuretags)) — can run to this **full** deadline before the gateway errors, so an unprovisioned-tag HistoryRead can block for ~30 s. Lower this to fail faster, at the cost of truncating legitimately long reads. |
|
||||
| `MaxTieClusterOverfetch` | int | `65536` | Maximum samples the server will fetch in one shot to page through a tie cluster (multiple samples sharing one `SourceTimestamp`). A cluster larger than this ceiling fails `BadHistoryOperationUnsupported`. Raise to handle abnormally large tie clusters; the default covers all normal-data cases. |
|
||||
|
||||
> **Do not commit `ApiKey` to `appsettings.json`.** Set it via the environment variable
|
||||
@@ -104,6 +104,33 @@ source it from there.
|
||||
|
||||
---
|
||||
|
||||
## Tag auto-provisioning (EnsureTags)
|
||||
|
||||
When a deploy adds a historized tag, the server auto-ensures it exists in the historian via the gateway's
|
||||
`EnsureTags` **before** the first value or HistoryRead lands. `AddressSpaceApplier.Apply()` fires a
|
||||
**non-blocking, fire-and-forget** `IHistorianProvisioning.EnsureTagsAsync` for each added historized value
|
||||
tag (with the resolved historian name — the `historianTagname` override when set, else the driver
|
||||
`FullName`); the hook is wrapped so a faulted, slow, or throwing provisioner can **never** block or fail a
|
||||
deploy. Non-numeric (`String`/`DateTime`/`Reference`) types are skipped, not provisioned.
|
||||
|
||||
**Wiring + gating.** The concrete provisioner is the gateway-backed `GatewayTagProvisioner`, registered by
|
||||
the Host's `AddHistorianProvisioning` (`GatewayHistorian.CreateProvisioner`) and gated on the **same**
|
||||
`ServerHistorian:Enabled` flag as the read path — both target the same single gateway. When the section is
|
||||
disabled (or absent) the applier resolves the no-op `NullHistorianProvisioning` and **provisioning does not
|
||||
run** (so on a read-only-historian deployment, ensure tags exist by some other means). The Host requires the
|
||||
API key to carry the `historian:tags:write` scope for `EnsureTags` to succeed.
|
||||
|
||||
**Observability.** Every dispatch logs an Information tally —
|
||||
`historian provisioning completed (dispatched=N, requested=…, ensured=…, skipped=…, failed=…)`. A line
|
||||
showing `dispatched=N` but `requested=0` indicates the **no-op** provisioner ran (provisioning is not wired
|
||||
for this node), which is the signal to check that `ServerHistorian:Enabled=true`.
|
||||
|
||||
> **Why this matters for HistoryRead latency.** Until a tag is provisioned, the historian does not know it,
|
||||
> so a HistoryRead on it runs to the full `CallTimeout` (~30 s) before erroring. Auto-provisioning on deploy
|
||||
> keeps freshly-historized tags from hitting that slow path. See the `CallTimeout` row above.
|
||||
|
||||
---
|
||||
|
||||
## HistoryRead behavior
|
||||
|
||||
### Read variants
|
||||
|
||||
Reference in New Issue
Block a user