This commit is contained in:
2026-05-13 09:38:52 +03:00
parent 24962fba03
commit d4805b06e6
15 changed files with 514 additions and 7 deletions
@@ -0,0 +1,133 @@
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);
}
}
+14
View File
@@ -0,0 +1,14 @@
using Microsoft.Extensions.Configuration;
namespace JobScheduler.Tasks;
/// <summary>
/// A named unit of work invoked by <see cref="JobScheduler.Scheduling.JobSchedulerHostedService"/>.
/// </summary>
public interface IJobTask
{
/// <summary>Matches <c>Jobs:Tasks:*:TaskType</c> in configuration.</summary>
string TaskType { get; }
Task ExecuteAsync(IConfiguration parametersSection, CancellationToken cancellationToken);
}
+17
View File
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>JobScheduler</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
</Project>