90 lines
4.1 KiB
C#
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);
|
|
}
|
|
}
|