diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs index 623414df..2e29f03a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs @@ -797,36 +797,20 @@ akka {{ _logger.LogInformation("SiteReplicationActor created and S&F replication handler wired"); - // Create the Deployment Manager as a cluster singleton - var singletonProps = ClusterSingletonManager.Props( - singletonProps: Props.Create(() => new DeploymentManagerActor( - storage, - compilationService, - sharedScriptLibrary, - streamManager, - siteRuntimeOptionsValue, - dmLogger, - dclManager, - replicationActor, - siteHealthCollector, - _serviceProvider, - null, - deploymentConfigFetcher)), - terminationMessage: PoisonPill.Instance, - settings: ClusterSingletonManagerSettings.Create(_actorSystem!) - .WithRole(siteRole) - .WithSingletonName("deployment-manager")); - - _actorSystem!.ActorOf(singletonProps, "deployment-manager-singleton"); - - // Create a proxy for other actors to communicate with the singleton - var proxyProps = ClusterSingletonProxy.Props( - singletonManagerPath: "/user/deployment-manager-singleton", - settings: ClusterSingletonProxySettings.Create(_actorSystem) - .WithRole(siteRole) - .WithSingletonName("deployment-manager")); - - var dmProxy = _actorSystem.ActorOf(proxyProps, "deployment-manager-proxy"); + // Deployment Manager — role-scoped singleton via SingletonRegistrar + // (review 01 round-2 N5): previously hand-rolled with bare PoisonPill + // termination and NO PhaseClusterLeave drain, so in-flight SQLite + // writes (static overrides, native_alarm_state) could be cut off on + // graceful failover. Names unchanged: deployment-manager-singleton / + // deployment-manager-proxy. + var dm = SingletonRegistrar.Start( + _actorSystem!, "deployment-manager", + Props.Create(() => new DeploymentManagerActor( + storage, compilationService, sharedScriptLibrary, streamManager, + siteRuntimeOptionsValue, dmLogger, dclManager, replicationActor, + siteHealthCollector, _serviceProvider, null, deploymentConfigFetcher)), + _logger, role: siteRole); + var dmProxy = dm.Proxy; // Create SiteCommunicationActor for receiving messages from central var siteCommActor = _actorSystem.ActorOf( @@ -847,22 +831,11 @@ akka {{ var eventLogQueryService = _serviceProvider.GetService(); if (eventLogQueryService != null) { - var eventLogSingletonProps = ClusterSingletonManager.Props( - singletonProps: Props.Create(() => new SiteEventLogging.EventLogHandlerActor(eventLogQueryService)), - terminationMessage: PoisonPill.Instance, - settings: ClusterSingletonManagerSettings.Create(_actorSystem) - .WithRole(siteRole) - .WithSingletonName("event-log-handler")); - _actorSystem.ActorOf(eventLogSingletonProps, "event-log-handler-singleton"); - - var eventLogProxyProps = ClusterSingletonProxy.Props( - singletonManagerPath: "/user/event-log-handler-singleton", - settings: ClusterSingletonProxySettings.Create(_actorSystem) - .WithRole(siteRole) - .WithSingletonName("event-log-handler")); - var eventLogProxy = _actorSystem.ActorOf(eventLogProxyProps, "event-log-handler-proxy"); - - siteCommActor.Tell(new RegisterLocalHandler(LocalHandlerType.EventLog, eventLogProxy)); + var eventLog = SingletonRegistrar.Start( + _actorSystem, "event-log-handler", + Props.Create(() => new SiteEventLogging.EventLogHandlerActor(eventLogQueryService)), + _logger, role: siteRole); + siteCommActor.Tell(new RegisterLocalHandler(LocalHandlerType.EventLog, eventLog.Proxy)); } // Parked message handler — bridges Akka to StoreAndForwardService diff --git a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CoordinatedShutdownTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CoordinatedShutdownTests.cs index 3e0fe0fb..5b1a51ac 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CoordinatedShutdownTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CoordinatedShutdownTests.cs @@ -61,11 +61,29 @@ public class CoordinatedShutdownTests Assert.Contains($"\"{name}\"", content); } - // No hand-rolled ClusterSingletonManager registrations remain in the - // CENTRAL branch. The only two left are the SITE singletons - // (deployment-manager, event-log-handler), which stay hand-rolled because - // they are role-scoped (.WithRole(siteRole)). - Assert.Equal(2, CountOccurrences(content, "ClusterSingletonManager.Props(")); + // No hand-rolled ClusterSingletonManager registrations remain anywhere: + // the two role-scoped SITE singletons now also go through the registrar + // (SingletonRegistrar.Start(..., role: siteRole)), so every singleton in + // the file is created through the registrar with a drain task. + Assert.Equal(0, CountOccurrences(content, "ClusterSingletonManager.Props(")); + } + + [Fact] + public void SiteSingletons_RegisterThroughRegistrarWithDrain() + { + var hostProjectDir = FindHostProjectDirectory(); + Assert.NotNull(hostProjectDir); + var content = File.ReadAllText(Path.Combine(hostProjectDir!, "Actors", "AkkaHostedService.cs")); + + // Round-2 N5: the two role-scoped SITE singletons previously stayed + // hand-rolled (bare PoisonPill, no PhaseClusterLeave drain). They now + // go through SingletonRegistrar.Start(..., role: siteRole), which + // always adds the GracefulStop drain — in-flight SQLite writes + // (static overrides, native_alarm_state) complete before handover. + Assert.Contains("\"deployment-manager\"", content); + Assert.Contains("\"event-log-handler\"", content); + Assert.Contains("role: siteRole", content); + Assert.Equal(0, CountOccurrences(content, "ClusterSingletonManager.Props(")); } private static int CountOccurrences(string haystack, string needle)