using JobScheduler.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace JobScheduler.Scheduling; /// /// Loads Jobs:Tasks and runs each enabled task on its own interval. /// public sealed class JobSchedulerHostedService : BackgroundService { private readonly IConfiguration _configuration; private readonly IReadOnlyDictionary _tasksByType; private readonly ILogger _logger; public JobSchedulerHostedService( IConfiguration configuration, IEnumerable tasks, ILogger logger) { _configuration = configuration; _tasksByType = tasks.ToDictionary(t => t.TaskType, t => t, StringComparer.OrdinalIgnoreCase); _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var section = _configuration.GetSection("Jobs:Tasks"); var children = section.GetChildren().ToList(); if (children.Count == 0) { _logger.LogWarning("No Jobs:Tasks configured; scheduler idle."); await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken); return; } var loops = new List(); foreach (var taskSection in children) { if (!taskSection.GetValue("Enabled", true)) { _logger.LogInformation("Job task disabled: {Section}", taskSection.Path); continue; } var taskType = taskSection["TaskType"]; if (string.IsNullOrWhiteSpace(taskType)) { _logger.LogWarning("Skipping task without TaskType at {Section}", taskSection.Path); continue; } if (!_tasksByType.TryGetValue(taskType, out var task)) { _logger.LogError("No IJobTask registered for TaskType '{TaskType}'.", taskType); continue; } var interval = ParseInterval(taskSection["Interval"]); var parameters = taskSection.GetSection("Parameters"); loops.Add(RunTaskLoopAsync(taskType, task, parameters, interval, stoppingToken)); } if (loops.Count == 0) { _logger.LogWarning("No enabled job tasks to run."); await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken); return; } await Task.WhenAll(loops); } private async Task RunTaskLoopAsync( string taskType, IJobTask task, IConfiguration parameters, TimeSpan interval, CancellationToken stoppingToken) { _logger.LogInformation( "Starting job loop for {TaskType} every {Interval}.", taskType, interval); try { while (!stoppingToken.IsCancellationRequested) { try { await task.ExecuteAsync(parameters, stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { throw; } catch (Exception ex) { _logger.LogError(ex, "Job task {TaskType} failed.", taskType); } if (interval <= TimeSpan.Zero) { _logger.LogWarning( "Job task {TaskType} has non-positive Interval; sleeping 1 hour.", taskType); await Task.Delay(TimeSpan.FromHours(1), stoppingToken); continue; } await Task.Delay(interval, stoppingToken); } } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { _logger.LogInformation("Job loop for {TaskType} cancelled.", taskType); } } private static TimeSpan ParseInterval(string? value) { if (string.IsNullOrWhiteSpace(value)) { return TimeSpan.FromHours(1); } return TimeSpan.TryParse(value, out var ts) ? ts : TimeSpan.FromHours(1); } }