using Akka.Actor; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace ZB.MOM.WW.OtOpcUa.Host; /// /// Down-if-alone recovery watchdog (arch-review #459). The 2-node keep-oldest split-brain /// resolver downs the lone survivor when the OLDEST node crashes (the survivor is "the side /// without the oldest" → DownReachable), and run-coordinated-shutdown-when-down = on /// terminates that node's . Recovery is by exit-and-rejoin: the /// service supervisor (Windows sc.exe failure … restart / docker restart: unless-stopped) /// restarts the exited node and it re-forms / rejoins as a fresh incarnation. /// /// This watchdog closes the gap where the terminates but the .NET /// host process keeps running with a dead actor system (idling forever, never restarted). It watches /// ; if the system terminates outside a normal host /// shutdown, it stops the application so the supervisor can restart the process. Mirrors the sister /// ScadaBridge project's proven pattern. /// /// Registered after AddAkka so it starts after Akka's own hosted service has /// built the system; the is resolved lazily in /// (never at construction) so it can't race Akka startup. A graceful host stop is distinguished from /// an unexpected SBR self-down via plus /// , so normal shutdown never logs a false /// alarm or double-triggers . /// public sealed class ActorSystemTerminationWatchdog : IHostedService { private readonly Func _actorSystemAccessor; private readonly IHostApplicationLifetime _lifetime; private readonly ILogger _logger; private volatile bool _stopRequested; /// Constructs the watchdog over a lazy accessor and the host lifetime. /// Lazy accessor (resolved in , never at construction, so it can't race Akka startup). /// Host application lifetime used to stop the process on an unexpected self-down. /// Logger for the critical self-down diagnostic. public ActorSystemTerminationWatchdog( Func actorSystemAccessor, IHostApplicationLifetime lifetime, ILogger logger) { _actorSystemAccessor = actorSystemAccessor; _lifetime = lifetime; _logger = logger; } /// Wires the continuation that exits the host on an unexpected self-down. /// Unused; the watchdog only registers a continuation. /// A completed task. public Task StartAsync(CancellationToken cancellationToken) { var system = _actorSystemAccessor(); system.WhenTerminated.ContinueWith( _ => { // Expected shutdown: our StopAsync ran, or the host is already stopping. Stay quiet. if (_stopRequested || _lifetime.ApplicationStopping.IsCancellationRequested) return; _logger.LogCritical( "ActorSystem terminated outside host shutdown (SBR self-down / " + "run-coordinated-shutdown-when-down). Stopping the host so the service supervisor " + "restarts this node as a fresh incarnation (2-node keep-oldest exit-and-rejoin recovery)."); _lifetime.StopApplication(); }, TaskContinuationOptions.ExecuteSynchronously); return Task.CompletedTask; } /// Marks a graceful shutdown so the termination continuation stays silent and does not re-trigger stop. /// Unused. /// A completed task. public Task StopAsync(CancellationToken cancellationToken) { _stopRequested = true; return Task.CompletedTask; } }