Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundMethodWarmupService.cs
T

90 lines
4.1 KiB
C#

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;
/// <summary>
/// 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 <c>/health/*</c> (review 2026-10-21 05/U3).
/// It used to run synchronously in <c>Program.cs</c> before <c>app.RunAsync()</c>, i.e. before
/// any hosted service started.
/// <para>
/// A node only executes inbound methods while it is the active central node, so the warm-up
/// waits (polling <see cref="IActiveNodeGate"/>) 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 <see cref="InboundScriptExecutor"/> remains the correctness
/// guarantee — this is purely an optimisation for the first requests.
/// </para>
/// </summary>
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<InboundMethodWarmupService> _logger;
/// <summary>Initializes the warm-up service.</summary>
/// <param name="scopeFactory">Creates the scope used to read the method list.</param>
/// <param name="executor">The executor whose handler cache is warmed.</param>
/// <param name="lifetime">Host lifetime; the warm-up starts only after ApplicationStarted.</param>
/// <param name="logger">Logger for warm-up diagnostics.</param>
/// <param name="gate">Optional active-node gate; absent means always active.</param>
public InboundMethodWarmupService(
IServiceScopeFactory scopeFactory,
InboundScriptExecutor executor,
IHostApplicationLifetime lifetime,
ILogger<InboundMethodWarmupService> logger,
IActiveNodeGate? gate = null)
{
_scopeFactory = scopeFactory;
_executor = executor;
_lifetime = lifetime;
_logger = logger;
_gate = gate;
}
/// <inheritdoc />
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<IInboundApiRepository>();
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);
}
}