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.");
}
}
}
}