Files
Cleanuparr/code/Executable/Jobs/BackgroundJobManager.cs
2025-06-15 21:15:50 +03:00

233 lines
7.7 KiB
C#

using Common.Configuration;
using Data.Models.Configuration.DownloadCleaner;
using Data.Models.Configuration.QueueCleaner;
using Common.Exceptions;
using Common.Helpers;
using Data;
using Infrastructure.Verticals.DownloadCleaner;
using Infrastructure.Verticals.Jobs;
using Infrastructure.Verticals.QueueCleaner;
using Microsoft.EntityFrameworkCore;
using Quartz;
using Quartz.Spi;
namespace Executable.Jobs;
/// <summary>
/// Manages background jobs in the application.
/// This class is responsible for reading configurations and scheduling jobs.
/// </summary>
public class BackgroundJobManager : IHostedService
{
private readonly ISchedulerFactory _schedulerFactory;
private readonly DataContext _dataContext;
private readonly ILogger<BackgroundJobManager> _logger;
private IScheduler? _scheduler;
public BackgroundJobManager(
ISchedulerFactory schedulerFactory,
DataContext dataContext,
ILogger<BackgroundJobManager> logger
)
{
_schedulerFactory = schedulerFactory;
_dataContext = dataContext;
_logger = logger;
}
/// <summary>
/// Starts the background job manager.
/// This method is called when the application starts.
/// </summary>
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting BackgroundJobManager");
_scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
await InitializeJobsFromConfiguration(cancellationToken);
_logger.LogInformation("BackgroundJobManager started");
}
/// <summary>
/// Stops the background job manager.
/// This method is called when the application stops.
/// </summary>
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping BackgroundJobManager");
if (_scheduler != null)
{
// Don't shutdown the scheduler as it's managed by QuartzHostedService
await _scheduler.Standby(cancellationToken);
}
_logger.LogInformation("BackgroundJobManager stopped");
}
/// <summary>
/// Initializes jobs based on current configuration settings.
/// </summary>
private async Task InitializeJobsFromConfiguration(CancellationToken cancellationToken = default)
{
if (_scheduler == null)
{
throw new InvalidOperationException("Scheduler not initialized");
}
// Get configurations from db
QueueCleanerConfig queueCleanerConfig = await _dataContext.QueueCleanerConfigs
.AsNoTracking()
.FirstAsync(cancellationToken);
DownloadCleanerConfig downloadCleanerConfig = await _dataContext.DownloadCleanerConfigs
.AsNoTracking()
.FirstAsync(cancellationToken);
await AddQueueCleanerJob(queueCleanerConfig, cancellationToken);
await AddDownloadCleanerJob(downloadCleanerConfig, cancellationToken);
}
/// <summary>
/// Adds the QueueCleaner job to the scheduler.
/// </summary>
public async Task AddQueueCleanerJob(
QueueCleanerConfig config,
CancellationToken cancellationToken = default)
{
if (!config.Enabled)
{
return;
}
await AddJobWithTrigger<QueueCleaner>(
config,
config.CronExpression,
cancellationToken);
}
/// <summary>
/// Adds the DownloadCleaner job to the scheduler.
/// </summary>
public async Task AddDownloadCleanerJob(DownloadCleanerConfig config, CancellationToken cancellationToken = default)
{
if (!config.Enabled)
{
return;
}
await AddJobWithTrigger<DownloadCleaner>(
config,
config.CronExpression,
cancellationToken);
}
/// <summary>
/// Helper method to add a job with a cron trigger.
/// </summary>
private async Task AddJobWithTrigger<T>(
IJobConfig config,
string cronExpression,
CancellationToken cancellationToken = default)
where T : GenericHandler
{
if (_scheduler == null)
{
throw new InvalidOperationException("Scheduler not initialized");
}
if (!config.Enabled)
{
return;
}
string typeName = typeof(T).Name;
var jobKey = new JobKey(typeName);
// Create job detail
var jobDetail = JobBuilder.Create<GenericJob<T>>()
.StoreDurably()
.WithIdentity(jobKey)
.Build();
// Add job to scheduler
await _scheduler.AddJob(jobDetail, true, cancellationToken);
// Validate the cron expression
if (!string.IsNullOrEmpty(cronExpression))
{
IOperableTrigger triggerObj = (IOperableTrigger)TriggerBuilder.Create()
.WithIdentity("ValidationTrigger")
.StartNow()
.WithCronSchedule(cronExpression)
.Build();
IReadOnlyList<DateTimeOffset> nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2);
TimeSpan triggerValue = nextFireTimes[1] - nextFireTimes[0];
if (triggerValue > Constants.TriggerMaxLimit)
{
throw new ValidationException($"{cronExpression} should have a fire time of maximum {Constants.TriggerMaxLimit.TotalHours} hours");
}
if (triggerValue < Constants.TriggerMinLimit)
{
throw new ValidationException($"{cronExpression} should have a fire time of minimum {Constants.TriggerMinLimit.TotalSeconds} seconds");
}
if (triggerValue > StaticConfiguration.TriggerValue)
{
StaticConfiguration.TriggerValue = triggerValue;
}
}
// Create cron trigger
var trigger = TriggerBuilder.Create()
.WithIdentity($"{typeName}-trigger")
.ForJob(jobKey)
.WithCronSchedule(cronExpression, x => x.WithMisfireHandlingInstructionDoNothing())
.StartNow()
.Build();
// Create startup trigger to run immediately
var startupTrigger = TriggerBuilder.Create()
.WithIdentity($"{typeName}-startup-trigger")
.ForJob(jobKey)
.StartNow()
.Build();
// Schedule job with both triggers
await _scheduler.ScheduleJob(trigger, cancellationToken);
await _scheduler.ScheduleJob(startupTrigger, cancellationToken);
_logger.LogInformation("Added job {name} with cron expression {CronExpression}",
typeName, cronExpression);
}
/// <summary>
/// Helper method to add a job without a trigger (for chained jobs).
/// </summary>
private async Task AddJobWithoutTrigger<T>(CancellationToken cancellationToken = default)
where T : GenericHandler
{
if (_scheduler == null)
{
throw new InvalidOperationException("Scheduler not initialized");
}
string typeName = typeof(T).Name;
var jobKey = new JobKey(typeName);
// Create job detail that is durable (can exist without triggers)
var jobDetail = JobBuilder.Create<GenericJob<T>>()
.WithIdentity(jobKey)
.StoreDurably()
.Build();
// Add job to scheduler
await _scheduler.AddJob(jobDetail, true, cancellationToken);
_logger.LogInformation("Added job {name} without trigger (will be chained)", typeName);
}
}