feat: replace ActorSelection with ClusterClient for inter-cluster communication

Central and site clusters now communicate via ClusterClient/
ClusterClientReceptionist instead of direct ActorSelection. Both
CentralCommunicationActor and SiteCommunicationActor are registered
with their cluster's receptionist. Central creates one ClusterClient
per site using NodeA/NodeB contact points from the DB. Sites configure
multiple CentralContactPoints for automatic failover between central
nodes. ISiteClientFactory enables test injection.
This commit is contained in:
Joseph Doherty
2026-03-18 00:08:47 -04:00
parent e5eb871961
commit 4f22ca2b1f
15 changed files with 287 additions and 136 deletions

View File

@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using Akka.Actor;
using Akka.Cluster;
using Akka.Cluster.Tools.Client;
@@ -168,10 +169,15 @@ akka {{
/// </summary>
private void RegisterCentralActors()
{
var siteClientFactory = new DefaultSiteClientFactory();
var centralCommActor = _actorSystem!.ActorOf(
Props.Create(() => new CentralCommunicationActor(_serviceProvider)),
Props.Create(() => new CentralCommunicationActor(_serviceProvider, siteClientFactory)),
"central-communication");
// Register CentralCommunicationActor with ClusterClientReceptionist so site ClusterClients can reach it
ClusterClientReceptionist.Get(_actorSystem).RegisterService(centralCommActor);
_logger.LogInformation("CentralCommunicationActor registered with ClusterClientReceptionist");
// Wire up the CommunicationService with the actor reference
var commService = _serviceProvider.GetService<CommunicationService>();
commService?.SetCommunicationActor(centralCommActor);
@@ -247,26 +253,37 @@ akka {{
var dmProxy = _actorSystem.ActorOf(proxyProps, "deployment-manager-proxy");
// WP-4: Create SiteCommunicationActor for receiving messages from central
_actorSystem.ActorOf(
var siteCommActor = _actorSystem.ActorOf(
Props.Create(() => new SiteCommunicationActor(
_nodeOptions.SiteId!,
_communicationOptions,
dmProxy)),
"site-communication");
// Register SiteCommunicationActor with ClusterClientReceptionist so central ClusterClients can reach it
ClusterClientReceptionist.Get(_actorSystem).RegisterService(siteCommActor);
_logger.LogInformation(
"Site actors registered. DeploymentManager singleton scoped to role={SiteRole}, SiteCommunicationActor created.",
siteRole);
// Register with Central if configured — tells Central where to send deployment commands
if (!string.IsNullOrWhiteSpace(_communicationOptions.CentralActorPath))
// Create ClusterClient to central if contact points are configured
if (_communicationOptions.CentralContactPoints.Count > 0)
{
var siteCommActor = _actorSystem.ActorSelection("/user/site-communication");
siteCommActor.Tell(new RegisterCentralPath(_communicationOptions.CentralActorPath));
var contacts = _communicationOptions.CentralContactPoints
.Select(cp => ActorPath.Parse($"{cp}/system/receptionist"))
.ToImmutableHashSet();
var clientSettings = ClusterClientSettings.Create(_actorSystem)
.WithInitialContacts(contacts);
var centralClient = _actorSystem.ActorOf(
ClusterClient.Props(clientSettings), "central-cluster-client");
var siteCommSelection = _actorSystem.ActorSelection("/user/site-communication");
siteCommSelection.Tell(new RegisterCentralClient(centralClient));
_logger.LogInformation(
"Configured central heartbeat path at {CentralPath} for site {SiteId}",
_communicationOptions.CentralActorPath, _nodeOptions.SiteId);
"Created ClusterClient to central with {Count} contact point(s) for site {SiteId}",
contacts.Count, _nodeOptions.SiteId);
}
}
}

View File

@@ -30,7 +30,10 @@
"ReplicationEnabled": true
},
"Communication": {
"CentralActorPath": "akka.tcp://scadalink@localhost:8081/user/central-communication",
"CentralContactPoints": [
"akka.tcp://scadalink@localhost:8081",
"akka.tcp://scadalink@localhost:8082"
],
"DeploymentTimeout": "00:02:00",
"LifecycleTimeout": "00:00:30",
"QueryTimeout": "00:00:30",