fix: wire up health report pipeline between sites and central aggregator

Sites now send SiteHealthReport via AkkaHealthReportTransport →
SiteCommunicationActor → CentralCommunicationActor → CentralHealthAggregator.
Added IHealthReportTransport impl, ISiteIdentityProvider impl, registered
HealthReportSender on site nodes, and added SiteHealthReport handler in
CentralCommunicationActor. Health Dashboard now shows all 3 sites online.
This commit is contained in:
Joseph Doherty
2026-03-17 23:46:17 -04:00
parent 9e97c1acd2
commit e5eb871961
5 changed files with 70 additions and 3 deletions

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Messages.Communication;
using ScadaLink.Commons.Messages.Health;
using ScadaLink.HealthMonitoring;
namespace ScadaLink.Communication.Actors;
@@ -50,8 +51,9 @@ public class CentralCommunicationActor : ReceiveActor
// Periodic refresh trigger
Receive<RefreshSiteAddresses>(_ => LoadSiteAddressesFromDb());
// Site registration via heartbeats
// Health monitoring: heartbeats and health reports from sites
Receive<HeartbeatMessage>(HandleHeartbeat);
Receive<SiteHealthReport>(HandleSiteHealthReport);
// Connection state changes
Receive<ConnectionStateChanged>(HandleConnectionStateChanged);
@@ -62,10 +64,23 @@ public class CentralCommunicationActor : ReceiveActor
private void HandleHeartbeat(HeartbeatMessage heartbeat)
{
// Forward heartbeat to parent/subscribers (central health monitoring)
// Forward heartbeat to parent for any interested central actors
Context.Parent.Tell(heartbeat);
}
private void HandleSiteHealthReport(SiteHealthReport report)
{
var aggregator = _serviceProvider.GetService<ICentralHealthAggregator>();
if (aggregator != null)
{
aggregator.ProcessReport(report);
}
else
{
_log.Warning("ICentralHealthAggregator not available, dropping health report from site {0}", report.SiteId);
}
}
private void HandleConnectionStateChanged(ConnectionStateChanged msg)
{
if (!msg.IsConnected)

View File

@@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="../ScadaLink.Commons/ScadaLink.Commons.csproj" />
<ProjectReference Include="../ScadaLink.HealthMonitoring/ScadaLink.HealthMonitoring.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,29 @@
using Akka.Actor;
using ScadaLink.Commons.Messages.Health;
using ScadaLink.HealthMonitoring;
using ScadaLink.Host.Actors;
namespace ScadaLink.Host;
/// <summary>
/// Sends SiteHealthReport to the local SiteCommunicationActor via Akka ActorSelection.
/// The SiteCommunicationActor forwards it to central.
/// </summary>
public class AkkaHealthReportTransport : IHealthReportTransport
{
private readonly AkkaHostedService _akkaService;
public AkkaHealthReportTransport(AkkaHostedService akkaService)
{
_akkaService = akkaService;
}
public void Send(SiteHealthReport report)
{
var actorSystem = _akkaService.ActorSystem;
if (actorSystem == null) return;
var siteComm = actorSystem.ActorSelection("/user/site-communication");
siteComm.Tell(report, ActorRefs.NoSender);
}
}

View File

@@ -151,10 +151,14 @@ try
// Shared components
services.AddClusterInfrastructure();
services.AddCommunication();
services.AddHealthMonitoring();
services.AddSiteHealthMonitoring();
services.AddExternalSystemGateway();
services.AddNotificationService();
// Health report transport: sends SiteHealthReport to SiteCommunicationActor via Akka
services.AddSingleton<ISiteIdentityProvider, SiteIdentityProvider>();
services.AddSingleton<IHealthReportTransport, AkkaHealthReportTransport>();
// Site-only components — AddSiteRuntime registers SiteStorageService with SQLite path
// and site-local repository implementations (IExternalSystemRepository, INotificationRepository)
var siteDbPath = context.Configuration["ScadaLink:Database:SiteDbPath"] ?? "site.db";

View File

@@ -0,0 +1,18 @@
using Microsoft.Extensions.Options;
using ScadaLink.HealthMonitoring;
namespace ScadaLink.Host;
/// <summary>
/// Provides the site identity from NodeOptions configuration.
/// </summary>
public class SiteIdentityProvider : ISiteIdentityProvider
{
public string SiteId { get; }
public SiteIdentityProvider(IOptions<NodeOptions> nodeOptions)
{
SiteId = nodeOptions.Value.SiteId
?? throw new InvalidOperationException("SiteId is required for site nodes.");
}
}