Phase 3A: Site runtime foundation — Akka cluster, SQLite persistence, Deployment Manager singleton, Instance Actor
- WP-1: Site cluster config (keep-oldest SBR, down-if-alone, 2s/10s failure detection) - WP-2: Site-role host bootstrap (no Kestrel, SQLite paths) - WP-3: SiteStorageService with deployed_configurations + static_attribute_overrides tables - WP-4: DeploymentManagerActor as cluster singleton with staggered Instance Actor creation, OneForOneStrategy/Resume supervision, deploy/disable/enable/delete lifecycle - WP-5: InstanceActor with attribute state, GetAttribute/SetAttribute, SQLite override persistence - WP-6: CoordinatedShutdown verified for graceful singleton handover - WP-7: Dual-node recovery (both seed nodes, min-nr-of-members=1) - WP-8: 31 tests (storage CRUD, actor lifecycle, supervision, negative checks) 389 total tests pass, zero warnings.
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Cluster;
|
||||
using Akka.Cluster.Tools.Singleton;
|
||||
using Akka.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ScadaLink.ClusterInfrastructure;
|
||||
using ScadaLink.Host.Actors;
|
||||
using ScadaLink.SiteRuntime;
|
||||
using ScadaLink.SiteRuntime.Actors;
|
||||
using ScadaLink.SiteRuntime.Persistence;
|
||||
|
||||
namespace ScadaLink.Host.Actors;
|
||||
|
||||
@@ -41,6 +46,10 @@ public class AkkaHostedService : IHostedService
|
||||
var seedNodesStr = string.Join(",",
|
||||
_clusterOptions.SeedNodes.Select(s => $"\"{s}\""));
|
||||
|
||||
// For site nodes, include a site-specific role (e.g., "site-SiteA") alongside the base role
|
||||
var roles = BuildRoles();
|
||||
var rolesStr = string.Join(",", roles.Select(r => $"\"{r}\""));
|
||||
|
||||
var hocon = $@"
|
||||
akka {{
|
||||
actor {{
|
||||
@@ -54,7 +63,7 @@ akka {{
|
||||
}}
|
||||
cluster {{
|
||||
seed-nodes = [{seedNodesStr}]
|
||||
roles = [""{_nodeOptions.Role}""]
|
||||
roles = [{rolesStr}]
|
||||
min-nr-of-members = {_clusterOptions.MinNrOfMembers}
|
||||
split-brain-resolver {{
|
||||
active-strategy = {_clusterOptions.SplitBrainResolverStrategy}
|
||||
@@ -78,8 +87,9 @@ akka {{
|
||||
_actorSystem = ActorSystem.Create("scadalink", config);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Akka.NET actor system 'scadalink' started. Role={Role}, Hostname={Hostname}, Port={Port}",
|
||||
"Akka.NET actor system 'scadalink' started. Role={Role}, Roles={Roles}, Hostname={Hostname}, Port={Port}",
|
||||
_nodeOptions.Role,
|
||||
string.Join(", ", roles),
|
||||
_nodeOptions.NodeHostname,
|
||||
_nodeOptions.RemotingPort);
|
||||
|
||||
@@ -90,6 +100,12 @@ akka {{
|
||||
Props.Create(() => new DeadLetterMonitorActor(dlmLogger)),
|
||||
"dead-letter-monitor");
|
||||
|
||||
// For site nodes, register the Deployment Manager as a cluster singleton
|
||||
if (_nodeOptions.Role.Equals("Site", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
RegisterSiteActors();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -103,4 +119,62 @@ akka {{
|
||||
_logger.LogInformation("Akka.NET actor system shutdown complete.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the list of cluster roles for this node. Site nodes get both "Site"
|
||||
/// and a site-specific role (e.g., "site-SiteA") to scope singleton placement.
|
||||
/// </summary>
|
||||
private List<string> BuildRoles()
|
||||
{
|
||||
var roles = new List<string> { _nodeOptions.Role };
|
||||
|
||||
if (_nodeOptions.Role.Equals("Site", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrEmpty(_nodeOptions.SiteId))
|
||||
{
|
||||
roles.Add($"site-{_nodeOptions.SiteId}");
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers site-specific actors including the Deployment Manager cluster singleton.
|
||||
/// The singleton is scoped to the site-specific cluster role so it runs on exactly
|
||||
/// one node within this site's cluster.
|
||||
/// </summary>
|
||||
private void RegisterSiteActors()
|
||||
{
|
||||
var siteRole = $"site-{_nodeOptions.SiteId}";
|
||||
var storage = _serviceProvider.GetRequiredService<SiteStorageService>();
|
||||
var siteRuntimeOptionsValue = _serviceProvider.GetService<IOptions<SiteRuntimeOptions>>()?.Value
|
||||
?? new SiteRuntimeOptions();
|
||||
var dmLogger = _serviceProvider.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger<DeploymentManagerActor>();
|
||||
|
||||
// Create the Deployment Manager as a cluster singleton
|
||||
var singletonProps = ClusterSingletonManager.Props(
|
||||
singletonProps: Props.Create(() => new DeploymentManagerActor(
|
||||
storage,
|
||||
siteRuntimeOptionsValue,
|
||||
dmLogger)),
|
||||
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"));
|
||||
|
||||
_actorSystem.ActorOf(proxyProps, "deployment-manager-proxy");
|
||||
|
||||
_logger.LogInformation(
|
||||
"Site actors registered. DeploymentManager singleton scoped to role={SiteRole}",
|
||||
siteRole);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user