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