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;
}
}