using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
///
/// Pre-compiles every inbound API method script in the background AFTER the host has started,
/// so the warm-up never delays the cluster join or /health/* (review 2026-10-21 05/U3).
/// It used to run synchronously in Program.cs before app.RunAsync(), i.e. before
/// any hosted service started.
///
/// A node only executes inbound methods while it is the active central node, so the warm-up
/// waits (polling ) until this node is active and then runs once;
/// a standby therefore pays nothing until a failover promotes it. The per-request lazy compile
/// plus revision check in remains the correctness
/// guarantee — this is purely an optimisation for the first requests.
///
///
public sealed class InboundMethodWarmupService : BackgroundService
{
private static readonly TimeSpan ActivePollInterval = TimeSpan.FromSeconds(5);
private readonly IServiceScopeFactory _scopeFactory;
private readonly InboundScriptExecutor _executor;
private readonly IHostApplicationLifetime _lifetime;
private readonly IActiveNodeGate? _gate;
private readonly ILogger _logger;
/// Initializes the warm-up service.
/// Creates the scope used to read the method list.
/// The executor whose handler cache is warmed.
/// Host lifetime; the warm-up starts only after ApplicationStarted.
/// Logger for warm-up diagnostics.
/// Optional active-node gate; absent means always active.
public InboundMethodWarmupService(
IServiceScopeFactory scopeFactory,
InboundScriptExecutor executor,
IHostApplicationLifetime lifetime,
ILogger logger,
IActiveNodeGate? gate = null)
{
_scopeFactory = scopeFactory;
_executor = executor;
_lifetime = lifetime;
_logger = logger;
_gate = gate;
}
///
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using (_lifetime.ApplicationStarted.Register(() => started.TrySetResult()))
using (stoppingToken.Register(() => started.TrySetCanceled(stoppingToken)))
{
await started.Task.ConfigureAwait(false);
}
while (_gate is { IsActiveNode: false })
await Task.Delay(ActivePollInterval, stoppingToken).ConfigureAwait(false);
await Task.Run(() => WarmAsync(stoppingToken), stoppingToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// shutting down
}
catch (Exception ex)
{
// Best effort: lazy compile on first request still guarantees correctness.
_logger.LogWarning(ex, "Inbound API method warm-up failed; methods will compile on first request.");
}
}
private async Task WarmAsync(CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService();
var methods = await repo.GetAllApiMethodsAsync(ct).ConfigureAwait(false);
// CompileAndRegister is safe for concurrent callers and single-flights per revision.
Parallel.ForEach(methods, new ParallelOptions { CancellationToken = ct },
method => _executor.CompileAndRegister(method));
_logger.LogInformation("Inbound API warm-up compiled {Count} method(s)", methods.Count);
}
}