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