Phase 1 WP-11–22: Host infrastructure, Blazor Server UI, and integration tests
Host infrastructure (WP-11–17): - StartupValidator with 19 validation rules - /health/ready endpoint with DB + Akka health checks - Akka.NET bootstrap via AkkaHostedService (HOCON config, cluster, remoting, SBR) - Serilog with SiteId/NodeHostname/NodeRole enrichment - DeadLetterMonitorActor with count tracking - CoordinatedShutdown wiring (no Environment.Exit) - Windows Service support (UseWindowsService) Central UI (WP-18–21): - Blazor Server shell with Bootstrap 5, role-aware NavMenu - Login/logout flow (LDAP auth → JWT → HTTP-only cookie) - CookieAuthenticationStateProvider with idle timeout - LDAP group mapping CRUD page (Admin role) - Route guards with Authorize attributes per role - SignalR reconnection overlay for failover Integration tests (WP-22): - Startup validation, auth flow, audit transactions, readiness gating 186 tests pass (1 skipped: LDAP integration), zero warnings.
This commit is contained in:
106
src/ScadaLink.Host/Actors/AkkaHostedService.cs
Normal file
106
src/ScadaLink.Host/Actors/AkkaHostedService.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ScadaLink.ClusterInfrastructure;
|
||||
using ScadaLink.Host.Actors;
|
||||
|
||||
namespace ScadaLink.Host.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// Hosted service that manages the Akka.NET actor system lifecycle.
|
||||
/// Creates the actor system on start, registers actors, and triggers
|
||||
/// CoordinatedShutdown on stop.
|
||||
/// </summary>
|
||||
public class AkkaHostedService : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly NodeOptions _nodeOptions;
|
||||
private readonly ClusterOptions _clusterOptions;
|
||||
private readonly ILogger<AkkaHostedService> _logger;
|
||||
private ActorSystem? _actorSystem;
|
||||
|
||||
public AkkaHostedService(
|
||||
IServiceProvider serviceProvider,
|
||||
IOptions<NodeOptions> nodeOptions,
|
||||
IOptions<ClusterOptions> clusterOptions,
|
||||
ILogger<AkkaHostedService> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_nodeOptions = nodeOptions.Value;
|
||||
_clusterOptions = clusterOptions.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the actor system once started. Null before StartAsync completes.
|
||||
/// </summary>
|
||||
public ActorSystem? ActorSystem => _actorSystem;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var seedNodesStr = string.Join(",",
|
||||
_clusterOptions.SeedNodes.Select(s => $"\"{s}\""));
|
||||
|
||||
var hocon = $@"
|
||||
akka {{
|
||||
actor {{
|
||||
provider = cluster
|
||||
}}
|
||||
remote {{
|
||||
dot-netty.tcp {{
|
||||
hostname = ""{_nodeOptions.NodeHostname}""
|
||||
port = {_nodeOptions.RemotingPort}
|
||||
}}
|
||||
}}
|
||||
cluster {{
|
||||
seed-nodes = [{seedNodesStr}]
|
||||
roles = [""{_nodeOptions.Role}""]
|
||||
min-nr-of-members = {_clusterOptions.MinNrOfMembers}
|
||||
split-brain-resolver {{
|
||||
active-strategy = {_clusterOptions.SplitBrainResolverStrategy}
|
||||
stable-after = {_clusterOptions.StableAfter.TotalSeconds:F0}s
|
||||
keep-oldest {{
|
||||
down-if-alone = on
|
||||
}}
|
||||
}}
|
||||
failure-detector {{
|
||||
heartbeat-interval = {_clusterOptions.HeartbeatInterval.TotalSeconds:F0}s
|
||||
acceptable-heartbeat-pause = {_clusterOptions.FailureDetectionThreshold.TotalSeconds:F0}s
|
||||
}}
|
||||
run-coordinated-shutdown-when-down = on
|
||||
}}
|
||||
coordinated-shutdown {{
|
||||
run-by-clr-shutdown-hook = on
|
||||
}}
|
||||
}}";
|
||||
|
||||
var config = ConfigurationFactory.ParseString(hocon);
|
||||
_actorSystem = ActorSystem.Create("scadalink", config);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Akka.NET actor system 'scadalink' started. Role={Role}, Hostname={Hostname}, Port={Port}",
|
||||
_nodeOptions.Role,
|
||||
_nodeOptions.NodeHostname,
|
||||
_nodeOptions.RemotingPort);
|
||||
|
||||
// Register the dead letter monitor actor
|
||||
var loggerFactory = _serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var dlmLogger = loggerFactory.CreateLogger<DeadLetterMonitorActor>();
|
||||
_actorSystem.ActorOf(
|
||||
Props.Create(() => new DeadLetterMonitorActor(dlmLogger)),
|
||||
"dead-letter-monitor");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_actorSystem != null)
|
||||
{
|
||||
_logger.LogInformation("Shutting down Akka.NET actor system via CoordinatedShutdown...");
|
||||
var shutdown = Akka.Actor.CoordinatedShutdown.Get(_actorSystem);
|
||||
await shutdown.Run(Akka.Actor.CoordinatedShutdown.ClrExitReason.Instance);
|
||||
_logger.LogInformation("Akka.NET actor system shutdown complete.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user