Files
lmxopcua/docs/v2/Runtime.md
T
Joseph Doherty 1ea1da8236 refactor(runtime): delete the dead in-place ApplyDelta seam; config change = respawn only (#665)
Since #516 a DriverConfig change is stop + respawn, so DriverSpawnPlan.ToApplyDelta
was always empty and the in-place path had no producer. #663 and #664 were both
latent bugs in it. This deletes it:
- host: ApplyChildDelta, _pendingDelta, HandleApplyResult and HandleDeltaApplied
- plan: DriverSpawnPlan.ToApplyDelta
- actor: the ApplyDelta, ApplyResult and DeltaApplied messages, the Connected
  reinit handler and the mid-connect AdoptConfigDuringInit
- tests: the actor tests that drove the seam

IDriver.ReinitializeAsync and each driver's implementation and unit tests stay.
Comments and docs that cited AdoptConfigDuringInit as the reason a driver must
release prior state at init now cite the IDriver contract and overlapping
retries, which still apply.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-09 11:23:16 -04:00

7.6 KiB

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, config-change diff, bad-quality-on-disconnect, write path, supervisor backoff) is stubbed — tracked as F7. Tests exercise message contracts, not engine behaviour.

VirtualTagActor / ScriptedAlarmActor

Both are fully wired in production (F8 + F9 shipped). VirtualTagActor compiles and evaluates expressions; ScriptedAlarmActor owns the per-alarm Part 9 state machine and persists ScriptedAlarmState to the config DB.

alarm-commands topic (inbound operator ack/shelve)

ScriptedAlarmHostActor subscribes to the alarm-commands DPS topic. Two surfaces publish onto this topic:

  • OPC UA Part 9 method path — OtOpcUaNodeManager handles Acknowledge / Confirm / AddComment / OneShotShelve / TimedShelve / Unshelve calls from external OPC UA clients. Each call is gated on the AlarmAck LDAP role (fail-closed); on allow, a Commons.OpcUa.AlarmCommand is published onto the topic.
  • AdminUI /alerts path — AdminOperationsActor (cluster singleton) publishes AcknowledgeAlarmCommand / ShelveAlarmCommand from the AdminUI operator buttons.

ScriptedAlarmHostActor ownership-filters incoming commands (each node acts only on its own alarms) and dispatches to the matching ScriptedAlarmEngine operation. The engine's OnEvent callback handles the resulting OPC UA condition-node update.

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 — gRPC to the ZB.MOM.WW.HistorianGateway sidecar (the sole historian backend; the named-pipe Wonderware sidecar is retired) + LocalDbStoreAndForwardSink (F11; renamed from SqliteStoreAndForwardSink when the buffer moved into the consolidated LocalDb).
  • PeerOpcUaProbeActor — real opc.tcp://peer:4840 ping (F12). Current stub always returns Ok=true.

DbHealthProbeActor

Ask<DbHealthStatus> returns cached state (refreshed every 5 s by an internal SELECT 1). Consumed by /health/ready and RedundancyStateActor.

Lifecycle wiring

// 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<OtOpcUaConfigDbContext> + 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.