using GB5Shared.DTO.Framework.Login; using JobEngineDAL.DTOs; using JobEngineDAL.Interfaces; using JobEngineBLL.Interfaces; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Quartz; using Quartz.Impl.Matchers; namespace JobEngineSL.Services { // Singleton service that manages the Quartz IScheduler. // Implements IQuartzSyncService so BLL can call hot-reload without Quartz dependency. // At startup: loads all active jobs from MJOBDEFINE and schedules them. // On SaveJob/PauseJob/ResumeJob/DeleteJob: called to update Quartz in real-time. public class QuartzSyncService : IQuartzSyncService { private readonly IScheduler _scheduler; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; private const string GroupName = "gb5"; public QuartzSyncService( IScheduler scheduler, IServiceScopeFactory scopeFactory, ILogger logger) { _scheduler = scheduler; _scopeFactory = scopeFactory; _logger = logger; } public async Task LoadAllJobsAsync(CancellationToken ct) { using var scope = _scopeFactory.CreateScope(); var dal = scope.ServiceProvider.GetRequiredService(); var sysLogin = new LoginDTO { ClientId = 0, UserId = 0 }; var jobs = await dal.GetAllActiveJobsAsync(sysLogin, ct).ConfigureAwait(false); foreach (var job in jobs) { try { await ScheduleOrRescheduleAsync(job, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Failed to schedule JobId {JobId} ({JobName}) at startup", job.JobId, job.JobName); } } _logger.LogInformation("QuartzSyncService loaded {Count} active jobs", jobs.Count()); } public async Task SyncJobAsync(int jobId, CancellationToken ct) { using var scope = _scopeFactory.CreateScope(); var dal = scope.ServiceProvider.GetRequiredService(); var sysLogin = new LoginDTO { ClientId = 0, UserId = 0 }; var job = await dal.GetJobDetailAsync(jobId, sysLogin, ct).ConfigureAwait(false); if (job == null) { await RemoveJobAsync(jobId, ct).ConfigureAwait(false); return; } if (job.Status != 1) { await RemoveJobAsync(jobId, ct).ConfigureAwait(false); return; } await ScheduleOrRescheduleAsync(job, ct).ConfigureAwait(false); } public async Task RemoveJobAsync(int jobId, CancellationToken ct) { var key = QuartzKey(jobId); if (await _scheduler.CheckExists(key, ct).ConfigureAwait(false)) await _scheduler.DeleteJob(key, ct).ConfigureAwait(false); } public async Task TriggerJobNowAsync(int jobId, CancellationToken ct) { var key = QuartzKey(jobId); if (await _scheduler.CheckExists(key, ct).ConfigureAwait(false)) { await _scheduler.TriggerJob(key, ct).ConfigureAwait(false); return; } // Job not scheduled (e.g. paused) — create a one-time trigger var detail = JobBuilder.Create() .WithIdentity($"manual-{jobId}-{DateTime.UtcNow.Ticks}", GroupName) .UsingJobData("JobId", jobId) .StoreDurably(false) .Build(); var trigger = TriggerBuilder.Create() .StartNow() .WithSimpleSchedule(s => s.WithRepeatCount(0)) .Build(); await _scheduler.ScheduleJob(detail, trigger, ct).ConfigureAwait(false); } private async Task ScheduleOrRescheduleAsync(JobDefineDTO job, CancellationToken ct) { var key = QuartzKey(job.JobId); var dataMap = new JobDataMap { { "JobId", job.JobId }, { "TenantId", job.TenantId } }; var jobBuilder = JobBuilder.Create() .WithIdentity(key) .UsingJobData(dataMap) .StoreDurably(); if (!job.IsConcurrent) jobBuilder = jobBuilder.DisallowConcurrentExecution(); var jobDetail = jobBuilder.Build(); ITrigger trigger = job.SchedulerType switch { "INTERVAL" => TriggerBuilder.Create() .WithIdentity($"trigger-{job.JobId}", GroupName) .WithSimpleSchedule(s => s .WithIntervalInSeconds(job.IntervalSeconds ?? 60) .RepeatForever()) .StartNow() .Build(), "ONE_TIME" => TriggerBuilder.Create() .WithIdentity($"trigger-{job.JobId}", GroupName) .StartAt(job.ScheduledDate.HasValue ? DateTimeOffset.FromFileTime(job.ScheduledDate.Value.ToFileTimeUtc()) : DateTimeOffset.UtcNow.AddSeconds(5)) .Build(), _ => TriggerBuilder.Create() // default CRON .WithIdentity($"trigger-{job.JobId}", GroupName) .WithCronSchedule(job.CronExpression ?? "0 0 * * * ?", c => c.InTimeZone(TimeZoneInfo.Utc)) .Build() }; if (await _scheduler.CheckExists(key, ct).ConfigureAwait(false)) await _scheduler.DeleteJob(key, ct).ConfigureAwait(false); await _scheduler.ScheduleJob(jobDetail, trigger, ct).ConfigureAwait(false); _logger.LogInformation("Scheduled JobId {JobId} ({JobName}) type={Type}", job.JobId, job.JobName, job.SchedulerType); } private static JobKey QuartzKey(int jobId) => new($"job-{jobId}", GroupName); } }