# OtOpcUa.Runtime Driver-role actor tree — one set per node. Path: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/`. ## Actor tree ``` DriverHostActor (per node) │ state machine: Steady ⇄ Applying ⇄ Stale │ ├──▶ DriverInstanceActor (per configured DriverInstance row) │ state: Connecting → Connected → Reconnecting (or Stubbed) │ ├──▶ VirtualTagActor (per VirtualTag row) │ compiles + evaluates expression, publishes derived value │ ├──▶ ScriptedAlarmActor (per ScriptedAlarm row) │ state: Inactive ⇄ Active ⇄ Acknowledged │ ├──▶ OpcUaPublishActor (per node, pinned dispatcher) │ marshalled OPC UA SDK writes + RebuildAddressSpace │ ├──▶ HistorianAdapterActor (per node) │ pipe IPC to Wonderware historian sidecar │ ├──▶ PeerOpcUaProbeActor (per peer node) │ opc.tcp ping → redundancy-state DPS topic │ └──▶ DbHealthProbeActor (per node) cached SELECT 1; consumed by /health/ready + redundancy calc ``` ## Public surface | Type | File | |---|---| | `WithOtOpcUaRuntimeActors()` | `ServiceCollectionExtensions.cs` — extension on `AkkaConfigurationBuilder`. Spawns `DriverHostActor` + `DbHealthProbeActor` on the host's ActorSystem. | | `DriverHostActor` | `Drivers/DriverHostActor.cs` | | `DriverInstanceActor` | `Drivers/DriverInstanceActor.cs` | | `VirtualTagActor` | `VirtualTags/VirtualTagActor.cs` | | `ScriptedAlarmActor` | `ScriptedAlarms/ScriptedAlarmActor.cs` | | `OpcUaPublishActor` | `OpcUa/OpcUaPublishActor.cs` | | `HistorianAdapterActor` | `Historian/HistorianAdapterActor.cs` | | `PeerOpcUaProbeActor` | `Health/PeerOpcUaProbeActor.cs` | | `DbHealthProbeActor` | `Health/DbHealthProbeActor.cs` | Marker keys for registry lookup: `DriverHostActorKey`, `DbHealthProbeActorKey`. ## DriverHostActor Per-node supervisor with three Become states: | State | Meaning | |---|---| | `Steady(rev)` | Caught up. `DispatchDeployment` with `msg.rev == currentRev` → immediate `ApplyAck(Applied)` (idempotent). New rev → `Become(Applying)`. | | `Applying(id)` | Apply in progress. Further `DispatchDeployment` for in-flight ID → debug-log + ignore. For new ID → defer via `Self.Forward`. | | `Stale` | ConfigDb unreachable on bootstrap. Periodic `RetryConfigDbConnection` tries to advance to `Steady`. | `PreStart`: 1. Subscribe to `deployments` DPS topic. 2. Read most-recent `NodeDeploymentState` for this node from ConfigDb. 3. If `Applied` → restore `_currentRevision`, `Become(Steady)`. 4. If `Applying` (orphan from crash) → replay apply (idempotent). 5. If `Failed` → `Become(Steady)` at last known rev. 6. DB unreachable → `Become(Stale)`, start retry timer. ACK publishing: when no `_coordinatorOverride` is set (production), `SendAck` publishes on the dedicated `deployment-acks` DPS topic which the coordinator subscribes to (commit `5cfbe8b`). ## DriverInstanceActor Per-driver-instance child. State machine: - `Connecting` → first attempt to reach the underlying driver - `Connected` → subscriptions active, reads/writes flow - `Reconnecting` → temporary disconnect; backoff retry - `Stubbed` → DEV-STUB mode for Windows-only drivers (Galaxy, Wonderware Historian) on non-Windows or when `roles` contains `dev` `ShouldStub(driverType, roles)` returns `true` for `"Galaxy" | "Historian.Wonderware"` on non-Windows; the actor goes straight to `Stubbed` and returns deterministic success without touching real hardware. Wiring this into the DriverHost child-spawn path is follow-up F20 (folds into F7). Engine wiring (subscription publishing, ApplyDelta diff, bad-quality-on-disconnect, write path, supervisor backoff) is stubbed — tracked as F7. Tests exercise message contracts, not engine behaviour. ## VirtualTagActor / ScriptedAlarmActor Skeleton state machines + message handlers. Engine work: - `VirtualTagEngine.Evaluate()` not yet called from `VirtualTagActor.DependencyValueChanged` (F8). - `AlarmConditionService` not yet called from `ScriptedAlarmActor` (F9). - `ScriptedAlarmState` DB persistence on `PreRestart` not wired (F9). ## OpcUaPublishActor The only actor on the **pinned dispatcher** (`opcua-synchronized-dispatcher` from `akka.conf`). All OPC UA SDK address-space writes go through it so the SDK's threading model isn't violated. Message contracts are defined; actual SDK calls are stubbed (counters only). Real address-space writes + `ServiceLevel` Variable updates + `RebuildAddressSpace` after a deploy land in F10 (gated on F13 — full `OpcUaApplicationHost` extraction). ## HistorianAdapterActor, PeerOpcUaProbeActor Both have message contracts wired. Engine integration deferred: - `HistorianAdapterActor` — named-pipe IPC to the Wonderware historian sidecar + `SqliteStoreAndForwardSink` (F11). - `PeerOpcUaProbeActor` — real `opc.tcp://peer:4840` ping (F12). Current stub always returns `Ok=true`. ## DbHealthProbeActor `Ask` returns cached state (refreshed every 5 s by an internal `SELECT 1`). Consumed by `/health/ready` and `RedundancyStateActor`. ## Lifecycle wiring ```csharp // Program.cs (driver role only) builder.Services.AddAkka("otopcua", (ab, sp) => { ab.WithOtOpcUaClusterBootstrap(sp); if (hasAdmin) ab.WithOtOpcUaControlPlaneSingletons(); if (hasDriver) ab.WithOtOpcUaRuntimeActors(); }); ``` `WithOtOpcUaRuntimeActors` resolves `IDbContextFactory` + `IClusterRoleInfo` from DI, then spawns `DbHealthProbeActor` and `DriverHostActor` as top-level `/user/` actors. Both register marker keys in `ActorRegistry` so the registry lookup works from anywhere. ## Tests `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/` — 16 tests covering DriverHostActor (Steady ack, Applying transitions, Stale recovery), DriverInstanceActor (state machine, stub mode), VirtualTagActor + ScriptedAlarmActor (message contracts), OpcUaPublishActor (props + message acceptance), DbHealthProbe + PeerOpcUaProbe (probe loop), and the `WithOtOpcUaRuntimeActors` registration round-trip. End-to-end deploy from admin → driver via the cluster is in `tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/DeployHappyPathTests.cs`.