using Akka.Actor; using Akka.Cluster.Tools.Singleton; using Microsoft.Extensions.Logging; namespace ZB.MOM.WW.ScadaBridge.Host.Actors; /// /// Registers a cluster singleton with the canonical naming scheme /// ({name}-singleton / {name}-proxy), a PoisonPill termination /// message, and a PhaseClusterLeave drain task that GracefulStops the manager /// so in-flight EF (central) or SQLite (site) work completes before handover. /// Extracted from copy-pasted ~60-line blocks (review 01 [Low]) whose drift /// left the two busiest singletons (notification-outbox, audit-log-ingest) /// without drain tasks (review 01 [Medium]). The optional role maps to /// / /// .WithRole, so role-scoped site singletons get the same drain /// (review 01 round-2 N5). /// internal static class SingletonRegistrar { internal sealed record Handle(IActorRef Manager, IActorRef Proxy); /// /// Creates the and proxy for a cluster /// singleton under the canonical naming scheme, and registers a /// PhaseClusterLeave drain task that GracefulStops the manager on shutdown. /// /// The actor system to register the singleton on. /// The singleton's base name (actors are named {name}-singleton / {name}-proxy). /// The used to create the singleton's managed actor. /// Logger used to report drain progress/failures. /// Maximum time to wait for the drain task to complete; defaults to 10 seconds. /// Optional cluster role to scope the singleton to (applied to both the manager and proxy settings). /// A carrying the singleton manager and proxy actor refs. internal static Handle Start( ActorSystem system, string name, Props singletonProps, ILogger logger, TimeSpan? drainTimeout = null, string? role = null) { var managerSettings = ClusterSingletonManagerSettings.Create(system).WithSingletonName(name); if (role is not null) managerSettings = managerSettings.WithRole(role); var manager = system.ActorOf( ClusterSingletonManager.Props( singletonProps, PoisonPill.Instance, managerSettings), $"{name}-singleton"); var timeout = drainTimeout ?? TimeSpan.FromSeconds(10); Akka.Actor.CoordinatedShutdown.Get(system).AddTask( Akka.Actor.CoordinatedShutdown.PhaseClusterLeave, $"drain-{name}-singleton", async () => { try { await manager.GracefulStop(timeout); } catch (Exception ex) { logger.LogWarning(ex, "{Singleton} singleton did not drain within the graceful-stop timeout; " + "falling through to PoisonPill handover", name); } return Akka.Done.Instance; }); var proxySettings = ClusterSingletonProxySettings.Create(system).WithSingletonName(name); if (role is not null) proxySettings = proxySettings.WithRole(role); var proxy = system.ActorOf( ClusterSingletonProxy.Props( $"/user/{name}-singleton", proxySettings), $"{name}-proxy"); return new Handle(manager, proxy); } }