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
This commit is contained in:
+11
-21
@@ -255,25 +255,15 @@ Things to know before touching any of this:
|
||||
instead of gating the feed for the life of the connection. OpcUaClient removes a half-built alarm
|
||||
subscription from its session when the subscribe throws, so those retries do not accumulate.
|
||||
End-to-end pin: `NativeAlarmFeedSurvivesDeployTests`.
|
||||
- **An in-place `ApplyDelta` on a Connected driver releases and re-establishes BOTH subscriptions (#663).**
|
||||
`ReinitializeAsync` drops every driver-side subscription (#630), but the handler used to keep the actor's
|
||||
handles, and the reconcile only re-subscribes a *missing* handle — so the driver went silent until a
|
||||
reconnect. It now releases data and alarms at the driver first (explicitly, so a driver whose reinit keeps
|
||||
its subscriptions is not left holding two), reinitialises, and re-subscribes both in a `finally` (a failed
|
||||
reinit has lost them too). It stays on `ReinitializeAsync` rather than shutdown + initialise because
|
||||
Calculation and Galaxy take their config at construction and ignore the JSON passed to `InitializeAsync`.
|
||||
Not reachable from a deploy since #516 (a DriverConfig change respawns). Pin:
|
||||
`An_in_place_ApplyDelta_leaves_one_live_data_and_one_live_alarm_subscription` (one case per wrong fix).
|
||||
- **A mid-connect `ApplyDelta` re-runs `InitializeAsync(newConfig)` — so every driver's `InitializeAsync` must
|
||||
honour the JSON (#664).** `AdoptConfigDuringInit` adopts a delta that lands while Connecting/Reconnecting by
|
||||
re-initialising, and acks on adoption. Every driver re-parses a config with a body (#516/#630) except two that
|
||||
used to ignore it: **Calculation** now rebuilds its tag table from it (sharing `AdoptConfig` with
|
||||
`ReinitializeAsync`, and disposing any running timer scheduler first), and **Galaxy**, which cannot hot-swap
|
||||
its options, now **refuses** a non-equivalent config with the same `NotSupportedException` its
|
||||
`ReinitializeAsync` throws — it used to connect on the old endpoint while the deploy reported success. A
|
||||
`"{}"`/empty document still keeps the constructor options. Once the adopted init succeeds the actor sends
|
||||
`DeltaApplied`, so the host re-registers a dependency consumer's mux interest — the mid-connect path used to
|
||||
send nothing. A new driver must follow the same rule. Unreachable from a deploy since #516.
|
||||
- **There is no in-place config path — a config change is a respawn (#665).** Since #516 any DriverConfig,
|
||||
DriverType or ResilienceConfig change is stop + respawn, so the factory is the single parse authority. The old
|
||||
in-place seam (host `ApplyChildDelta` → actor `ApplyDelta` → `ReinitializeAsync`, plus the mid-connect
|
||||
`AdoptConfigDuringInit`, `ApplyResult` and `DeltaApplied`) had no producer and was deleted after two latent
|
||||
bugs were found in it: a Connected reinit kept subscription handles the driver had dropped (#663), and the
|
||||
mid-connect path re-ran `InitializeAsync(newConfig)` on two drivers that ignored the JSON (#664). Kept from that
|
||||
work: Calculation's `InitializeAsync` adopts a config with a body and Galaxy's refuses a non-equivalent one,
|
||||
because the `IDriver` contract is that a driver serves the document it is handed. `ReinitializeAsync` stays as a
|
||||
per-driver contract the runtime does not call.
|
||||
- **Restart tests must assert a FUNCTION, never existence.** `RestartDriver_respawns_the_child_without_an_actor_name_collision`
|
||||
asserts only that a child exists after a restart, and it passed green against the dead driver 03/S16
|
||||
describes. It is kept — its own subject, the `InvalidActorNameException`, is real — but the load-bearing
|
||||
@@ -458,8 +448,8 @@ have leaked ~360 transports at the 10 s retry interval. The shutdown is best-eff
|
||||
on it still gets its initialise attempt.
|
||||
|
||||
**`InitializeAsync` itself releases the previous device/connection state first (#630).** The retry above is not
|
||||
the only way in: `DriverInstanceActor.AdoptConfigDuringInit` calls `InitializeAsync` on a live driver with *no*
|
||||
teardown, so a driver that does `_devices[host] = new DeviceState(...)` (or `Session = ...`, `Plc = ...`) orphans
|
||||
the only way in: the `IDriver` contract allows `InitializeAsync` on a live driver with *no* teardown, and a retry
|
||||
tick can overlap a still-running attempt, so a driver that does `_devices[host] = new DeviceState(...)` (or `Session = ...`, `Plc = ...`) orphans
|
||||
the previous state's probe / recycle / fixed-tree loops and its client. FOCAS, AbLegacy, AbCip, TwinCAT, S7 and
|
||||
OpcUaClient now cancel, bounded-join (5 s) and dispose that state at the top of `InitializeAsync`
|
||||
(`ReleaseDeviceStatesAsync` / `ReleaseConnectionAsync` / `ReleaseSessionAsync`), alongside Modbus and MQTT.
|
||||
|
||||
@@ -16,9 +16,9 @@ changes the served address space — the **deploy artifact** — and one advisor
|
||||
check (`FetchAndCache`) — see `docs/Configuration.md` §`ConfigSource`.
|
||||
3. **Apply.** `DriverHostActor` (`src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/`) opens the artifact and
|
||||
reconciles in two halves:
|
||||
- **Drivers** — `DriverSpawnPlanner.Compute` yields `ToStop` / `ToApplyDelta` / `ToSpawn`. A changed
|
||||
driver config reaches the live driver as `IDriver.ReinitializeAsync` on its `DriverInstanceActor`;
|
||||
a removed driver is stopped. An unreadable or empty blob is "no answer": the apply fails and the
|
||||
- **Drivers** — `DriverSpawnPlanner.Compute` yields `ToStop` / `ToSpawn`. A changed
|
||||
driver config (DriverConfig, DriverType or ResilienceConfig) is a stop + respawn (#516) — there is no
|
||||
in-place path (#665 deleted the unreachable `ApplyDelta` seam); a removed driver is stopped. An unreadable or empty blob is "no answer": the apply fails and the
|
||||
running children are left alone (never an empty configuration).
|
||||
- **Address space** — `AddressSpaceComposer` builds an `AddressSpaceComposition` from the artifact;
|
||||
`AddressSpacePlan` diffs it against the last applied composition; `AddressSpaceChangeClassifier`
|
||||
|
||||
@@ -415,7 +415,7 @@ down rather than swallowing it. A SUBACK failure degrades just the affected RawP
|
||||
There are **two** kinds of reconnect and only one of them fires that callback. A drop the connection's
|
||||
own supervisor recovers keeps the same `MqttConnection`, so `Reconnected` → `OnReconnectedAsync`
|
||||
re-establishes every live filter. A **session rebuild** — `ShutdownAsync` + `InitializeAsync` (the #527
|
||||
comms-loss watchdog's retry path, and an `ApplyDelta` that changes the endpoint or credentials) — builds
|
||||
comms-loss watchdog's retry path, and a `ReinitializeAsync` that changes the endpoint or credentials) — builds
|
||||
a *new* connection and re-attaches the *same* `MqttSubscriptionManager`, and the re-subscribe then comes
|
||||
from the host (`DriverInstanceActor.ResubscribeDesired`) rather than from the callback. `AttachTo`
|
||||
therefore **forgets the filters the previous session had established**: the dedupe set is a claim about
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ Per-driver-instance child. State machine:
|
||||
|
||||
`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.
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user