diff --git a/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md b/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md index c74e839d..b008f6bb 100644 --- a/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md +++ b/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md @@ -504,3 +504,20 @@ Deviation from the plan's literal step text (cosmetic, recorded for the reviewer step 1 says `_desiredByDriver` *replaces* `_lastComposition` at `:347-349` while T3 step 2 says T3 deletes `_lastComposition`. Keeping T2 compilable means the two fields coexist for one commit; `_lastComposition` and its assignment are deleted in T3 as T3's own step text specifies. + +### T3 — `SpawnChild` always sends the desired set; `_lastComposition` deleted + +The falsification for this production slice is **T5**, written and observed red afterwards (below) — +T3 lands the fix first because the plan sequences it that way and the ordering guard needs the whole +25-suite regression before any new test is trusted on top of it. + +**HIGH-RISK regression gate (every child's spawn-time state changes, deploy path included):** the +FULL `Runtime.Tests` project, not just the `DriverHostActor` filter — +**639 passed, 3 skipped, 0 failed, 55 s**. The 3 skips are pre-existing (`DeploymentArtifact*` parity +tests, skipped on the base commit too). + +`_lastComposition` is gone: `grep -c _lastComposition DriverHostActor.cs` → **0**. Its xmldoc had also +drifted (it claimed the field was set at the end of `PushDesiredSubscriptions`; the assignment was in +`ApplyDesiredSubscriptions`) — deleted with the field rather than corrected. The stale +"dropped-driver fallback" comment on the old `SendAuthoredOnly` local function was rewritten as the +`SendDesiredSubscriptions` remarks naming the three real consumers. diff --git a/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md.tasks.json b/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md.tasks.json index 582bcf35..2a3500fc 100644 --- a/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md.tasks.json +++ b/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md.tasks.json @@ -17,7 +17,7 @@ { "id": "T3", "subject": "SpawnChild always sends the desired set; delete the write-only _lastComposition (03/S16)", - "status": "pending", + "status": "done", "blockedBy": [ "T2" ] diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs index ff212655..84bf70b6 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs @@ -344,10 +344,6 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS /// the config (#516). Bounded by the number of driver children with a delta in flight. private readonly Dictionary _pendingDelta = new(); - /// The composition from the most-recent apply (set at the END of - /// ). Null until the first apply. - private AddressSpaceComposition? _lastComposition; - /// One driver child's desired subscription payload — exactly the /// the apply's bulk push sent it. /// The non-alarm raw tags' RawPaths this driver should keep subscribed. @@ -2721,11 +2717,6 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS _log.Info("DriverHost {Node}: applied {Count} Equipment ScriptedAlarm(s) to the ScriptedAlarm host", _localNode, composition.EquipmentScriptedAlarms.Count); } - - // Cache the applied composition LAST. Set here (not in HandleArtifactForApply) so both the fresh-apply and - // bootstrap-restore paths — which both route through this method — leave a current composition. - _lastComposition = composition; - } /// @@ -2827,6 +2818,21 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS _children[spec.DriverInstanceId] = new ChildEntry(child, spec, stub); + // EVERY spawn hands the child its desired set — deploy reconcile, operator Restart (03/S16), and the + // supervision reprovision. Sending it HERE rather than at each call site is the point: a child that + // exists without its subscription set is a dead driver, so the two facts are established together. + // Before this, the only sender was the apply's bulk push, so an operator's Restart produced a child + // that connected and then read nothing until the next deployment — the #488 periodic reconcile cannot + // help, because it early-returns on an empty desired set. + // + // ORDERING, deliberately: ApplyDriverSpecs (which spawns) runs BEFORE ApplyDesiredSubscriptions (which + // rebuilds _desiredByDriver), so a newly-spawned child is handed the PREVIOUS apply's set — or nothing, + // on the first deploy. That transient is superseded milliseconds later by the bulk push on the same + // sender→receiver pair (Akka guarantees per-pair ordering), and the child is still Connecting, where + // SetDesiredSubscriptions only STORES; the subscribe happens at Connected entry. So the stale set is + // never acted on. Sent after InitializeRequested above, which keeps init-before-subscribe ordering. + SendDesiredSubscriptions(child, spec.DriverInstanceId); + // v3 (WP7): if the (real, non-stub) driver consumes other tags' live values, spawn a mux-adapter // child that registers its dependency refs on the per-node dependency mux and forwards changes // into the driver. Requires a wired mux (dev/None deployments have none — then calc inputs simply