From ad2ab741ff7bbe31f37fd118917d3509726f005f Mon Sep 17 00:00:00 2001 From: TekH Date: Sun, 12 Jul 2026 00:04:13 +0200 Subject: [PATCH] feat: Add ProfileManager for automatic Hangfire job scheduling - Create ProfileManager background service that polls database every 1 second - Query active profiles with cron schedules via MediatR GetProfileQuery - Auto-create/update Hangfire recurring jobs using IRecurringJobManager - Jobs execute TriggeringDEXJobBatchCommand via IMediator dependency injection - Add profile caching with schedule change detection to avoid redundant updates - Create DtoExtensions with JobId() and ToJob() helper methods for profile-to-command conversion --- .../Extensions/DtoExtensions.cs | 21 +++++++ ECMJobRunner.WebCron/ProfileManager.cs | 59 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 ECMJobRunner.WebCron/Extensions/DtoExtensions.cs create mode 100644 ECMJobRunner.WebCron/ProfileManager.cs diff --git a/ECMJobRunner.WebCron/Extensions/DtoExtensions.cs b/ECMJobRunner.WebCron/Extensions/DtoExtensions.cs new file mode 100644 index 0000000..0441f38 --- /dev/null +++ b/ECMJobRunner.WebCron/Extensions/DtoExtensions.cs @@ -0,0 +1,21 @@ +using ECMJobRunner.Application.Common.Dtos; +using ECMJobRunner.Application.DEXJob.Commands; +using MediatR; + +namespace ECMJobRunner.WebCron.Extensions; + +public static class DtoExtensions +{ + public static string JobId(this CfgProfileDto profile) + { + return $"profile-{profile.Id}-{profile.ProfileName.Replace(' ', '_').ToLowerInvariant()}"; + } + + public static TriggeringDEXJobBatchCommand ToJob(this CfgProfileDto profile) + { + return new TriggeringDEXJobBatchCommand + { + ProfileId = profile.Id, + }; + } +} diff --git a/ECMJobRunner.WebCron/ProfileManager.cs b/ECMJobRunner.WebCron/ProfileManager.cs new file mode 100644 index 0000000..cd5731e --- /dev/null +++ b/ECMJobRunner.WebCron/ProfileManager.cs @@ -0,0 +1,59 @@ +using ECMJobRunner.Application.Common.Dtos; +using ECMJobRunner.Application.DEXJob.Commands; +using ECMJobRunner.Application.DEXJob.Queries; +using ECMJobRunner.WebCron.Extensions; +using Hangfire; +using MediatR; +using System.Collections.Concurrent; + +namespace ECMJobRunner.WebCron +{ + public class ProfileManager(ILogger Logger, IMediator Mediator, IRecurringJobManager JobManager) : BackgroundService + { + private readonly ConcurrentDictionary Profiles = new(); + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + + while (!stoppingToken.IsCancellationRequested) + { + if (Logger.IsEnabled(LogLevel.Information)) + { + Logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + } + + var profiles = await Mediator.Send(new GetProfileQuery() + { + Active = true, + IncludeSqlJobs = true + }, stoppingToken); + + foreach (var profile in profiles) + { + if (Profiles.TryGetValue(profile.JobId(), out var currentProfile) + && currentProfile.Schedule == profile.Schedule) + continue; + + // Add or update recurring job using MediatR command + JobManager.AddOrUpdate( + profile.JobId(), + mediator => mediator.Send(profile.ToJob(), CancellationToken.None), + profile.Schedule, + new RecurringJobOptions + { + TimeZone = TimeZoneInfo.Local + } + ); + + // Store/update in local cache + Profiles[profile.JobId()] = profile; + + Logger.LogInformation("Job {JobId} registered with schedule: {Schedule}", + profile.JobId(), profile.Schedule); + } + + await Task.Delay(1000, stoppingToken); + } + } + } +}