fix(host): site singletons (deployment-manager, event-log-handler) via SingletonRegistrar — PhaseClusterLeave drains for site SQLite writes (plan R2-01 T11)

This commit is contained in:
Joseph Doherty
2026-07-13 11:01:44 -04:00
parent 80e5b36852
commit 56d39b5fce
2 changed files with 42 additions and 51 deletions
@@ -797,36 +797,20 @@ akka {{
_logger.LogInformation("SiteReplicationActor created and S&F replication handler wired"); _logger.LogInformation("SiteReplicationActor created and S&F replication handler wired");
// Create the Deployment Manager as a cluster singleton // Deployment Manager — role-scoped singleton via SingletonRegistrar
var singletonProps = ClusterSingletonManager.Props( // (review 01 round-2 N5): previously hand-rolled with bare PoisonPill
singletonProps: Props.Create(() => new DeploymentManagerActor( // termination and NO PhaseClusterLeave drain, so in-flight SQLite
storage, // writes (static overrides, native_alarm_state) could be cut off on
compilationService, // graceful failover. Names unchanged: deployment-manager-singleton /
sharedScriptLibrary, // deployment-manager-proxy.
streamManager, var dm = SingletonRegistrar.Start(
siteRuntimeOptionsValue, _actorSystem!, "deployment-manager",
dmLogger, Props.Create(() => new DeploymentManagerActor(
dclManager, storage, compilationService, sharedScriptLibrary, streamManager,
replicationActor, siteRuntimeOptionsValue, dmLogger, dclManager, replicationActor,
siteHealthCollector, siteHealthCollector, _serviceProvider, null, deploymentConfigFetcher)),
_serviceProvider, _logger, role: siteRole);
null, var dmProxy = dm.Proxy;
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");
// Create SiteCommunicationActor for receiving messages from central // Create SiteCommunicationActor for receiving messages from central
var siteCommActor = _actorSystem.ActorOf( var siteCommActor = _actorSystem.ActorOf(
@@ -847,22 +831,11 @@ akka {{
var eventLogQueryService = _serviceProvider.GetService<SiteEventLogging.IEventLogQueryService>(); var eventLogQueryService = _serviceProvider.GetService<SiteEventLogging.IEventLogQueryService>();
if (eventLogQueryService != null) if (eventLogQueryService != null)
{ {
var eventLogSingletonProps = ClusterSingletonManager.Props( var eventLog = SingletonRegistrar.Start(
singletonProps: Props.Create(() => new SiteEventLogging.EventLogHandlerActor(eventLogQueryService)), _actorSystem, "event-log-handler",
terminationMessage: PoisonPill.Instance, Props.Create(() => new SiteEventLogging.EventLogHandlerActor(eventLogQueryService)),
settings: ClusterSingletonManagerSettings.Create(_actorSystem) _logger, role: siteRole);
.WithRole(siteRole) siteCommActor.Tell(new RegisterLocalHandler(LocalHandlerType.EventLog, eventLog.Proxy));
.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));
} }
// Parked message handler — bridges Akka to StoreAndForwardService // Parked message handler — bridges Akka to StoreAndForwardService
@@ -61,11 +61,29 @@ public class CoordinatedShutdownTests
Assert.Contains($"\"{name}\"", content); Assert.Contains($"\"{name}\"", content);
} }
// No hand-rolled ClusterSingletonManager registrations remain in the // No hand-rolled ClusterSingletonManager registrations remain anywhere:
// CENTRAL branch. The only two left are the SITE singletons // the two role-scoped SITE singletons now also go through the registrar
// (deployment-manager, event-log-handler), which stay hand-rolled because // (SingletonRegistrar.Start(..., role: siteRole)), so every singleton in
// they are role-scoped (.WithRole(siteRole)). // the file is created through the registrar with a drain task.
Assert.Equal(2, CountOccurrences(content, "ClusterSingletonManager.Props(")); 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) private static int CountOccurrences(string haystack, string needle)