using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace ScadaLink.HealthMonitoring; /// /// Central-side counterpart to . /// Periodically builds a SiteHealthReport for the central cluster itself /// (siteId = ) 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. /// public class CentralHealthReportLoop : BackgroundService { /// /// Reserved siteId used to represent the central cluster in the /// shared CentralHealthAggregator keyspace. /// public const string CentralSiteId = "central"; private readonly ISiteHealthCollector _collector; private readonly ICentralHealthAggregator _aggregator; private readonly IClusterNodeProvider _clusterNodeProvider; private readonly HealthMonitoringOptions _options; private readonly ILogger _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 options, ILogger logger, TimeProvider? timeProvider = null) { _collector = collector; _aggregator = aggregator; _clusterNodeProvider = clusterNodeProvider; _options = options.Value; _logger = logger; _sequenceNumber = (timeProvider ?? TimeProvider.System).GetUtcNow().ToUnixTimeMilliseconds(); } /// /// Current sequence number (for testing). /// 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"); } } } }