using FrameworkBLL.SchedulerTaskGenerator; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace FrameworkSL.Jobs { /// /// One-time startup migration: registers a native Quartz trigger for every /// currently-active MJOBDEFINE row across all tenants, via /// IQuartzSyncService.BootstrapAllAsync. /// /// Guarded by SchedulerRun:QuartzNativeBootstrapEnabled — keep this OFF until a /// throwaway synthetic test job has been verified to fire correctly exactly /// once via a manually-synced trigger (see the rollout/verification plan). /// Flipping it on with real MJOBDEFINE rows before that verification risks /// registering broken triggers for all of them at once. /// /// Must be registered AFTER AddQuartzHostedService in Program.cs so IScheduler /// is already started by the time this runs. /// public sealed class QuartzBootstrapHostedService : IHostedService { // IQuartzSyncService is registered Scoped (it consumes scoped services directly), // so this singleton-lifetime IHostedService must resolve it through its own scope // rather than injecting it into this class's constructor. private readonly IServiceScopeFactory _scopeFactory; private readonly IConfiguration _config; private readonly ILogger _logger; public QuartzBootstrapHostedService( IServiceScopeFactory scopeFactory, IConfiguration config, ILogger logger) { _scopeFactory = scopeFactory; _config = config; _logger = logger; } // BootstrapAllAsync walks every active tenant and every active job of each tenant, // doing several serial DB round-trips per job against the (often remote) Quartz ADO // store — with enough tenants/jobs this took multiple minutes and, being awaited // here, blocked the host from ever reaching "Now listening" until it finished. The // persistent job store means triggers already survive restarts (Program.cs's // UsePersistentStore comment), so this is a catch-up sync, not a prerequisite for // serving traffic — same reasoning RabbitMqPublisher.StartAsync already applied to // its own connect loop ("Fire-and-forget retry loop — app startup is not blocked"). public Task StartAsync(CancellationToken ct) { var enabled = string.Equals( _config["SchedulerRun:QuartzNativeBootstrapEnabled"], "Y", StringComparison.OrdinalIgnoreCase); if (!enabled) { _logger.LogInformation( "QuartzBootstrapHostedService: SchedulerRun:QuartzNativeBootstrapEnabled is not 'Y' — skipping bootstrap. No MJOBDEFINE jobs will get native Quartz triggers on this startup."); return Task.CompletedTask; } _ = RunBootstrapAsync(ct); return Task.CompletedTask; } private async Task RunBootstrapAsync(CancellationToken ct) { var sw = Stopwatch.StartNew(); _logger.LogInformation("QuartzBootstrapHostedService: starting bootstrap (background, not blocking startup)"); try { using var scope = _scopeFactory.CreateScope(); var quartzSync = scope.ServiceProvider.GetRequiredService(); await quartzSync.BootstrapAllAsync(ct).ConfigureAwait(false); _logger.LogInformation( "QuartzBootstrapHostedService: bootstrap complete in {ElapsedMs}ms", sw.ElapsedMilliseconds); } catch (OperationCanceledException) { } catch (Exception ex) { // Must not crash app startup — a bad bootstrap leaves Track A jobs // un-scheduled, which is recoverable (resave/resume any job to sync it // individually), not a reason to take the whole app down. _logger.LogError(ex, "QuartzBootstrapHostedService: bootstrap failed after {ElapsedMs}ms", sw.ElapsedMilliseconds); } } public Task StopAsync(CancellationToken ct) => Task.CompletedTask; } }