134 lines
4.3 KiB
C#
134 lines
4.3 KiB
C#
using JobScheduler.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace JobScheduler.Scheduling;
|
|
|
|
/// <summary>
|
|
/// Loads <c>Jobs:Tasks</c> and runs each enabled task on its own interval.
|
|
/// </summary>
|
|
public sealed class JobSchedulerHostedService : BackgroundService
|
|
{
|
|
private readonly IConfiguration _configuration;
|
|
private readonly IReadOnlyDictionary<string, IJobTask> _tasksByType;
|
|
private readonly ILogger<JobSchedulerHostedService> _logger;
|
|
|
|
public JobSchedulerHostedService(
|
|
IConfiguration configuration,
|
|
IEnumerable<IJobTask> tasks,
|
|
ILogger<JobSchedulerHostedService> 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<Task>();
|
|
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);
|
|
}
|
|
}
|