Files
lmxopcua/docs/ServiceHosting.md
T
Joseph Doherty 2124f21ab6
v2-ci / build (pull_request) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped
docs(historian-gateway): document gateway backend, config keys, EnsureTags hook, known gates; retire Wonderware from docs
HistorianGateway is now the sole historian backend (read + alarm SendEvent +
continuous WriteLiveValues). Document the final state and retire the Wonderware
sidecar from the docs/config/labels:

- CLAUDE.md: rewrite the Historian section — ServerHistorian /
  ContinuousHistorization / AlarmHistorian config keys, the IHistorianProvisioning
  EnsureTags hook, the GatewayAlarmHistorianWriter SendEvent path + ReadEvents
  dependency on gateway RuntimeDb:EventReadsEnabled=true, gateway-side
  prerequisites (RuntimeDb flags + historian:read/write/tags:write scopes),
  migration note, and two KNOWN-LIMITATION callouts (live-validation gate +
  empty historized-ref-set recorder follow-on).
- appsettings.json: fix the stale ServerHistorian block (Host/Port/SharedSecret/
  ServerCertThumbprint -> Endpoint/ApiKey/UseTls/AllowUntrustedServerCertificate/
  CaCertificatePath/CallTimeout, keep MaxTieClusterOverfetch); add a disabled
  ContinuousHistorization block; prune the orphaned Wonderware keys from
  AlarmHistorian (keep the SQLite knobs). ApiKey env-supplied via
  ServerHistorian__ApiKey (commented; valid strict JSON via _comment keys).
- README.md + docs (Historian.md, AlarmHistorian.md, Configuration.md,
  ServiceHosting.md, DriverLifecycle.md, drivers/README.md, Uns.md, VirtualTags.md,
  AlarmTracking.md, Client.UI.md, README.md, TestConnectProbes.md): retire the
  Wonderware historian backend from current-backend descriptions; fix the stale
  ServerHistorian/AlarmHistorian config tables (now gateway shape); convert
  drivers/Historian.Wonderware.md to a retired stub pointing at the gateway.
- Source/UI labels (descriptive text only, no behavior change):
  OtOpcUaServerHostedService.cs, HistoryPaging.cs, OtOpcUaSdkServer.cs,
  HistorianAdapterActor.cs, VirtualTagModal.razor, ScriptedAlarmModal.razor,
  AlarmsHistorian.razor now name the HistorianGateway backend.

Build clean (0 errors); AdminUI.Tests green (514 passed).

Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
2026-06-26 19:46:27 -04:00

6.1 KiB

Service Hosting (v2)

Overview

A production OtOpcUa deployment runs one binary per node. The historian backend is the external ZB.MOM.WW.HistorianGateway, deployed separately (not installed by this repo's scripts):

Process Project Runtime Platform Responsibility
OtOpcUa Host src/Server/ZB.MOM.WW.OtOpcUa.Host .NET 10 AnyCPU Single fused binary. OTOPCUA_ROLES env decides what to mount: admin (Blazor + auth + control-plane singletons), driver (OPC UA endpoint + per-driver actors), or both.
ZB.MOM.WW.HistorianGateway (external — separate deployment) not in this repo .NET 10 The sole historian backend. OtOpcUa talks gRPC to it (via the ZB.MOM.WW.HistorianGateway.Client package) for HistoryRead, alarm SendEvent, and continuous WriteLiveValues. Must run RuntimeDb:Enabled=true + RuntimeDb:EventReadsEnabled=true; the API key must carry historian:read + historian:write + historian:tags:write.

Galaxy access still uses the separately-installed mxaccessgw sidecar (see docs/v2/Galaxy.ParityRig.md); the gateway owns the MXAccess COM bitness constraint (its worker is x86 net48). Nothing in the OtOpcUa repo carries that constraint anymore. (The bespoke Wonderware historian sidecar this deployment used to ship was retired — see drivers/Historian.Wonderware.md.)

v2 change. v1's separate OtOpcUa.Server + OtOpcUa.Admin Windows services merged into a single role-gated OtOpcUa.Host binary. Two installers became one (with a -Roles parameter). The whole DI graph is composed in OtOpcUa.Host/Program.cs; per-role wiring is conditional on the env var.

Role gating

Program.cs reads OTOPCUA_ROLES, parses it with RoleParser, and conditionally registers services:

Role present Wires
admin AddOtOpcUaAuth, AddAdminUI, AddSignalR, AddOtOpcUaAdminClients, MapOtOpcUaAuth, MapAdminUI<App>, MapOtOpcUaHubs, WithOtOpcUaControlPlaneSingletons (5 admin singletons via Akka.Hosting)
driver WithOtOpcUaRuntimeActors (DriverHostActor + DbHealthProbeActor) — and the OPC UA endpoint on port 4840
Either / both AddOtOpcUaConfigDb, AddOtOpcUaCluster, AddOtOpcUaHealth (/health/ready, /health/active, /healthz)

Single-node dev: OTOPCUA_ROLES=admin,driver. Production: typically two admin nodes (HA pair) + N driver nodes.

Per-role configuration overlays

Program.cs:33-35 builds a role suffix by joining the parsed roles alphabetically with - and loads appsettings.{roleSuffix}.json as an optional overlay on top of base appsettings.json. Three overlays ship in src/Server/ZB.MOM.WW.OtOpcUa.Host/:

  • appsettings.admin.json — admin-only nodes
  • appsettings.driver.json — driver-only nodes
  • appsettings.admin-driver.json — fused single-node dev / small deployments

All three carry Serilog log-level overrides + Security:Ldap:DevStubMode = false. Configuration loading order (lowest to highest precedence) is:

appsettings.json < appsettings.{Environment}.json < appsettings.{role}.json < environment variables < command-line args

The role overlay intentionally outranks appsettings.{Environment}.json so role-level security defaults (such as DevStubMode = false) cannot be silently overridden by a developer's local appsettings.Development.json; environment variables and command-line args still outrank everything for deployment-level overrides. Overlays are optional; the base file boots a node on its own.

Akka cluster

The host joins an Akka.NET cluster bound to the address in appsettings.json::Cluster:

{
  "Cluster": {
    "Hostname": "0.0.0.0",
    "Port": 4053,
    "PublicHostname": "node-a.lan",
    "SeedNodes": ["akka.tcp://otopcua@node-a.lan:4053"],
    "Roles": ["admin", "driver"]
  }
}
  • WithOtOpcUaClusterBootstrap (in OtOpcUa.Cluster) loads the embedded HOCON (split-brain resolver, pinned dispatcher, failure detector tuning) and overlays remote endpoint + cluster options.
  • All cluster singletons + per-node actors live on this single ActorSystem — there is no second Akka instance.

See Redundancy.md for the role-leader + ServiceLevel story.

Health endpoints

Both admin and driver nodes expose:

Path Status meaning
/healthz Process alive.
/health/ready ConfigDb reachable + cluster member state is Up.
/health/active Admin-role leader (the node Traefik or an HA LB should route traffic to).

Used by Traefik for the active-leader-only routing pattern (see Architecture-v2.md).

Historian backend (HistorianGateway — external)

The historian backend is the external ZB.MOM.WW.HistorianGateway, deployed and operated separately (not installed by Install-Services.ps1). OtOpcUa connects to it over gRPC via the ZB.MOM.WW.HistorianGateway.Client package — configure the ServerHistorian:Endpoint (https://host:5222) and supply ServerHistorian__ApiKey via the environment on the OtOpcUa host side. The gateway must run with RuntimeDb:Enabled=true + RuntimeDb:EventReadsEnabled=true and an API key carrying historian:read + historian:write + historian:tags:write. See Historian.md for the full config-key and deployment-prerequisite reference. (The retired Wonderware TCP sidecar: Historian.Wonderware.md.)

Install / Uninstall

  • scripts/install/Install-Services.ps1 -Roles admin,driver — installs OtOpcUaHost.
  • scripts/install/Uninstall-Services.ps1 — stops + removes the host service. (The historian backend is the external HistorianGateway — not installed/removed by these scripts.)

Logging

Serilog with rolling-daily file sinks. Each host writes to logs/otopcua-*.log plus stdout (NSSM/systemd-friendly). Per-environment log level overrides go in appsettings.{Environment}.json.

Depth reference

For the full host-architecture rationale (why fused vs. split, role-gating tradeoffs, multi-node deployment shapes), see docs/plans/2026-05-26-akka-hosting-alignment-design.md §3-4.