diff --git a/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md b/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md index 4a0d7ace..6ab0d180 100644 --- a/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md +++ b/archreview/plans/R3-03-restart-resubscribe-watchdog-plan.md @@ -595,3 +595,13 @@ discipline for this assembly). works: with T3 reverted this test fails on the *positive control*, not on the absence. **Not covered, and worth a reviewer's eye:** the `_desiredByDriver` union's `_children.Keys` arm (a running child absent from BOTH ref maps) is not reachable through this seeder and is untested. + +### T6 — supervision reprovision hook + +`DriverInstanceActor.PostRestart` (calling `base.PostRestart(reason)` first, so `PreStart` re-attaches +the rediscovery / host-status sources and restarts the health timer) → `ReprovisionRequested` → +`DriverHostActor.HandleReprovisionRequested`, registered in **all three** `Receive` blocks that already +accept `RestartDriver`. The falsification is T7, below. + +Gate: `--filter "…~DriverHostActor|…~DriverInstanceActor"` → **183/183 passed** (179 before this task +plus T5's four), no test moved. 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 7023cf45..0e1d6c9f 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 @@ -40,7 +40,7 @@ { "id": "T6", "subject": "Supervision reprovision hook: DriverInstanceActor.PostRestart -> ReprovisionRequested -> host re-init + re-subscribe (03/S6, S16)", - "status": "pending", + "status": "done", "blockedBy": [ "T3" ] 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 9b58ed51..00352e15 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs @@ -1088,6 +1088,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS Receive(HandleDeltaApplied); Receive(HandleApplyResult); Receive(HandleRestartDriver); + Receive(HandleReprovisionRequested); Receive(HandleReconnectDriver); Receive(HandleRouteNodeWrite); Receive(HandleRouteNativeAlarmAck); @@ -1143,6 +1144,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS Receive(HandleDeltaApplied); Receive(HandleApplyResult); Receive(HandleRestartDriver); + Receive(HandleReprovisionRequested); Receive(HandleReconnectDriver); Receive(HandleRouteNodeWrite); Receive(HandleRouteNativeAlarmAck); @@ -1725,6 +1727,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS Receive(HandleGetDiagnostics); Receive(_ => TryRecoverFromStale()); Receive(HandleRestartDriver); + Receive(HandleReprovisionRequested); Receive(HandleReconnectDriver); Receive(OnRedundancyStateChanged); // The redundancy-state topic also carries OpcUaProbeResult (OpcUaPublishActor peer-probes). @@ -2991,6 +2994,40 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS SpawnChild(entry.Spec); } + /// + /// Re-provision a driver child that Akka's supervision just RESTARTED in place (archreview 03/S6, + /// 03/S16) — the third and least visible of the three incarnation routes. + /// + /// + /// A restart recreates the actor object around the SAME driver instance (the Props closure captured + /// it) without going through , so neither the InitializeRequested nor + /// the desired-subscription send that a spawn performs ever happens — and the child's + /// is unchanged, so _children needs no maintenance and the host has no + /// other notification that a new incarnation exists. Its PreRestart ran PostStop, which + /// called ShutdownAsync on the driver, so re-initialising is not optional: without it the + /// child sits in Connecting forever around a shut-down driver. + /// + /// The dependency-consumer mux adapter is a SEPARATE child of this host and is unaffected by the + /// instance actor restarting, so it is deliberately not touched here. + /// + /// + /// The restarted child's self-announcement. + private void HandleReprovisionRequested(DriverInstanceActor.ReprovisionRequested msg) + { + // Already stopped (a restart racing a reconcile that removed the driver) — nothing to provision. + if (!_children.TryGetValue(msg.DriverInstanceId, out var entry)) return; + + // A crash that Akka silently restarted is otherwise invisible to operators; name it. + _log.Warning( + "DriverHost {Node}: driver child {Id} was restarted by supervision after {Reason} — re-provisioning", + _localNode, msg.DriverInstanceId, msg.Reason); + + // A stub child has no real driver to initialise (its Stubbed behaviour no-ops InitializeRequested + // anyway), but it still stores a desired set, so only the initialise is conditional. + if (!entry.Stubbed) entry.Actor.Tell(new DriverInstanceActor.InitializeRequested(entry.Spec.DriverConfig)); + SendDesiredSubscriptions(entry.Actor, msg.DriverInstanceId); + } + private void HandleReconnectDriver(ReconnectDriver msg) { // DPS broadcast — only act if this node hosts the requested instance. diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs index 81852667..b53a1418 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs @@ -42,6 +42,25 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers /// restored → Good) — independently of alarm transitions, since a comms-lost driver emits no alarm /// events. Fire-and-forget, mirroring . public sealed record ConnectivityChanged(string DriverInstanceId, bool Connected); + + /// + /// Sent to the parent from so the host re-provisions THIS incarnation + /// (archreview 03/S6, 03/S16). + /// + /// + /// A supervision restart recreates the actor object in place: the desired subscription sets are gone + /// (they are instance fields), and PreRestart has already run — which + /// called on the driver object the Props closure re-uses. Nothing + /// else re-sends : its only sender is + /// DriverHostActor.SpawnChild, and a restart does NOT respawn (the child's + /// is unchanged, so the host is not otherwise notified that a new incarnation + /// exists at all). Without this the restarted child sits in Connecting forever around a + /// shut-down driver, with nothing subscribed — silently, since Akka logs the fault but the recovery + /// is invisible. + /// + /// The driver instance that was restarted. + /// The type name of the exception that tripped supervision, for the host's log. + public sealed record ReprovisionRequested(string DriverInstanceId, string Reason); public sealed record ApplyDelta(string DriverConfigJson, CorrelationId Correlation); public sealed record ApplyResult(bool Success, string? Reason, CorrelationId Correlation); /// @@ -375,6 +394,20 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers Timers.StartPeriodicTimer("health-poll", HealthPollTick.Instance, _healthPollInterval); } + /// + /// + /// Ask the host to re-provision this incarnation — see for why a + /// supervision restart otherwise leaves a dead child. base.PostRestart runs first because it + /// runs , which re-attaches the rediscovery / host-status sources and restarts + /// the health timer; the reprovision is the part cannot do, since only the + /// host knows this driver's config and desired set. + /// + protected override void PostRestart(Exception reason) + { + base.PostRestart(reason); + Context.Parent.Tell(new ReprovisionRequested(_driverInstanceId, reason.GetType().Name)); + } + private void Stubbed() { // Stubbed drivers accept the standard message contracts but return deterministic