92 lines
3.4 KiB
C#
92 lines
3.4 KiB
C#
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace ScadaLink.HealthMonitoring;
|
|
|
|
/// <summary>
|
|
/// Central-side counterpart to <see cref="HealthReportSender"/>.
|
|
/// Periodically builds a SiteHealthReport for the central cluster itself
|
|
/// (siteId = <see cref="CentralSiteId"/>) and feeds it into the local
|
|
/// CentralHealthAggregator so the UI can render central as another card
|
|
/// on /monitoring/health. Only the cluster leader (Primary) generates
|
|
/// reports — the standby's aggregator catches up on failover when it
|
|
/// becomes Primary and starts its own loop.
|
|
/// </summary>
|
|
public class CentralHealthReportLoop : BackgroundService
|
|
{
|
|
/// <summary>
|
|
/// Reserved siteId used to represent the central cluster in the
|
|
/// shared CentralHealthAggregator keyspace.
|
|
/// </summary>
|
|
public const string CentralSiteId = "central";
|
|
|
|
private readonly ISiteHealthCollector _collector;
|
|
private readonly ICentralHealthAggregator _aggregator;
|
|
private readonly IClusterNodeProvider _clusterNodeProvider;
|
|
private readonly HealthMonitoringOptions _options;
|
|
private readonly ILogger<CentralHealthReportLoop> _logger;
|
|
|
|
// Seeded with Unix-ms so reports from a newly-elected central leader
|
|
// always sort after reports from any prior leader for siteId="central".
|
|
// The clock is read through the injected TimeProvider so the seeding is
|
|
// deterministically testable.
|
|
private long _sequenceNumber;
|
|
|
|
public CentralHealthReportLoop(
|
|
ISiteHealthCollector collector,
|
|
ICentralHealthAggregator aggregator,
|
|
IClusterNodeProvider clusterNodeProvider,
|
|
IOptions<HealthMonitoringOptions> options,
|
|
ILogger<CentralHealthReportLoop> logger,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
_collector = collector;
|
|
_aggregator = aggregator;
|
|
_clusterNodeProvider = clusterNodeProvider;
|
|
_options = options.Value;
|
|
_logger = logger;
|
|
_sequenceNumber = (timeProvider ?? TimeProvider.System).GetUtcNow().ToUnixTimeMilliseconds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Current sequence number (for testing).
|
|
/// </summary>
|
|
public long CurrentSequenceNumber => Interlocked.Read(ref _sequenceNumber);
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
_logger.LogInformation(
|
|
"Central health report loop starting, interval {Interval}s",
|
|
_options.ReportInterval.TotalSeconds);
|
|
|
|
using var timer = new PeriodicTimer(_options.ReportInterval);
|
|
|
|
while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false))
|
|
{
|
|
try
|
|
{
|
|
var isPrimary = _clusterNodeProvider.SelfIsPrimary;
|
|
_collector.SetActiveNode(isPrimary);
|
|
|
|
if (!isPrimary)
|
|
continue;
|
|
|
|
_collector.SetClusterNodes(_clusterNodeProvider.GetClusterNodes());
|
|
|
|
var seq = Interlocked.Increment(ref _sequenceNumber);
|
|
var report = _collector.CollectReport(CentralSiteId);
|
|
var reportWithSeq = report with { SequenceNumber = seq };
|
|
|
|
_aggregator.ProcessReport(reportWithSeq);
|
|
|
|
_logger.LogDebug("Generated central health report #{Seq}", seq);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to generate central health report");
|
|
}
|
|
}
|
|
}
|
|
}
|