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