e08855fb9d
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.
132 lines
7.6 KiB
Markdown
132 lines
7.6 KiB
Markdown
# 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
|
|
|
|
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
|
|
|
|
```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<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`.
|