Adds deferment.md — a source-verified inventory of new-driver status, all 17
open issues, in-code deferrals, open live gates and plan bookkeeping. Every
claim was checked against src/ and git, not against documentation.
Headline finding: three subsystems are authored, persisted and shipped but
never executed —
* Node ACLs: IPermissionEvaluator/TriePermissionEvaluator have zero
production consumers and OtOpcUaNodeManager never references them, yet
ClusterAcls.razor authors NodeAcl rows and ConfigComposer.cs:51 ships them
in every artifact. An authored deny rule has no effect.
* IRediscoverable / IHostConnectivityProbe raise into the void (#518/#507);
nothing ever writes a DriverHostStatus row.
* DriverTypeRegistry is vestigial, so no factory passes a tier and every
driver runs Tier A with the Tier-C protections dormant.
Also records the Calculation driver as picker-visible but unauthorable
(DriverConfigModal has no case) — the "registered but unauthorable" class
recurring after the Sql picker defect — and notes that no parity test guards
DriverConfigModal/DeviceModal, which is why it survived review.
Documentation corrections (source-verified):
* ReadWriteOperations.md claimed "a denied read never hits the driver" via
four types that do not exist in src/. Bannered + struck through; a security
review reading that page would have concluded a per-node ACL gate exists.
* CLAUDE.md: the Change Detection sentence was false (DriverHost consumes
nothing); the mesh Phase 4 and Phase 5 live gates and the auto-down 1-vs-1
gate had all PASSED; the ScriptedAlarmState table was already dropped.
* driver-expansion tracking: Modbus RTU and SQL poll are merged, not
pending — its command table would have made someone rebuild two shipped
drivers in a fresh worktree.
* drivers/README.md: dead DriverTypeRegistry paragraph, retired
SystemPlatform namespace kind, missing Sql + Calculation rows, missing
Modbus RTU-over-TCP transport.
* TwinCAT.md/Galaxy.md promised an address-space rebuild that never happens.
* Historian.md gained the #491 unproven-value-capture pointer.
* IncrementalSync.md and AddressSpace.md bannered as wholesale v2-era (the
rewrite is recorded as still owed, not done here); four v2 status docs
bannered as historical; three wrong-name-for-a-live-type fixes.
Left deliberately untouched: the secrets NoOpSecretReplicator line, which
makes a security claim and needs the real behaviour identified rather than a
rename.
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:
- Subscribe to
deploymentsDPS topic. - Read most-recent
NodeDeploymentStatefor this node from ConfigDb. - If
Applied→ restore_currentRevision,Become(Steady). - If
Applying(orphan from crash) → replay apply (idempotent). - If
Failed→Become(Steady)at last known rev. - 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 driverConnected→ subscriptions active, reads/writes flowReconnecting→ temporary disconnect; backoff retryStubbed→ DEV-STUB mode for Windows-only drivers (Galaxy, Wonderware Historian) on non-Windows or whenrolescontainsdev
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
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 —
OtOpcUaNodeManagerhandles Acknowledge / Confirm / AddComment / OneShotShelve / TimedShelve / Unshelve calls from external OPC UA clients. Each call is gated on theAlarmAckLDAP role (fail-closed); on allow, aCommons.OpcUa.AlarmCommandis published onto the topic. - AdminUI
/alertspath —AdminOperationsActor(cluster singleton) publishesAcknowledgeAlarmCommand/ShelveAlarmCommandfrom 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 theZB.MOM.WW.HistorianGatewaysidecar (the sole historian backend; the named-pipe Wonderware sidecar is retired) +LocalDbStoreAndForwardSink(F11; renamed fromSqliteStoreAndForwardSinkwhen the buffer moved into the consolidated LocalDb).PeerOpcUaProbeActor— realopc.tcp://peer:4840ping (F12). Current stub always returnsOk=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.