using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.SiteEventLogging; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// /// WP3.2 (site_events volume policy — docs/plans/2026-08-15-site-events-policy-design.md): /// site-only hosted service that periodically flushes /// into one aggregate "script" Info site event. Registered by /// ServiceCollectionExtensions.AddSiteRuntime, which only the site composition root /// calls (SiteServiceRegistration.Configure) — central never runs scripts, so central /// never registers this service, which is what makes it "Site nodes only" per the design memo. /// /// The interval is read from on every tick (not /// captured once at construction) so /// is hot-reloadable — an operator can shorten or lengthen it, or disable it (0), live. /// Mirrors ScriptSchedulerStatsReporter's shape: fixed cadence, exceptions logged and /// swallowed so the loop survives every flush failure. /// public sealed class ScriptRunSummaryFlushService : BackgroundService { /// Poll cadence used while summaries are disabled (ScriptRunSummaryIntervalSeconds == 0) — coarse enough to amortise re-checking whether the interval was re-enabled live. private static readonly TimeSpan DisabledPollInterval = TimeSpan.FromSeconds(30); private readonly ScriptRunSummaryRecorder _recorder; private readonly ISiteEventLogger _siteEventLogger; private readonly IOptionsMonitor _optionsMonitor; private readonly ILogger _logger; /// Initializes a new instance of . /// The recorder whose accumulated counters this service flushes. /// The site event logger the flushed summary row is written to. /// Supplies the hot-reloadable flush interval. /// Logger instance. public ScriptRunSummaryFlushService( ScriptRunSummaryRecorder recorder, ISiteEventLogger siteEventLogger, IOptionsMonitor optionsMonitor, ILogger logger) { _recorder = recorder ?? throw new ArgumentNullException(nameof(recorder)); _siteEventLogger = siteEventLogger ?? throw new ArgumentNullException(nameof(siteEventLogger)); _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var intervalSeconds = _optionsMonitor.CurrentValue.ScriptRunSummaryIntervalSeconds; var wait = intervalSeconds > 0 ? TimeSpan.FromSeconds(intervalSeconds) : DisabledPollInterval; try { await Task.Delay(wait, stoppingToken).ConfigureAwait(false); } catch (OperationCanceledException) { break; } if (intervalSeconds <= 0) { // Summaries disabled for this tick's window: counters keep accumulating in // the recorder (harmless — the next enabled flush just reports a longer // window), we simply don't emit or reset yet. continue; } try { await _recorder.FlushAsync(_siteEventLogger).ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "ScriptRunSummaryFlushService flush failed; next tick will retry."); } } } }