Files
git.stella-ops.org/src/StellaOps.Excititor.Worker/Scheduling/VexWorkerHostedService.cs
2025-10-18 20:44:59 +03:00

111 lines
4.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Worker.Options;
namespace StellaOps.Excititor.Worker.Scheduling;
internal sealed class VexWorkerHostedService : BackgroundService
{
private readonly IOptions<VexWorkerOptions> _options;
private readonly IVexProviderRunner _runner;
private readonly ILogger<VexWorkerHostedService> _logger;
private readonly TimeProvider _timeProvider;
public VexWorkerHostedService(
IOptions<VexWorkerOptions> options,
IVexProviderRunner runner,
ILogger<VexWorkerHostedService> logger,
TimeProvider timeProvider)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_runner = runner ?? throw new ArgumentNullException(nameof(runner));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var schedules = _options.Value.ResolveSchedules();
if (schedules.Count == 0)
{
_logger.LogWarning("Excititor worker has no configured provider schedules; the service will remain idle.");
await Task.CompletedTask;
return;
}
_logger.LogInformation("Excititor worker starting with {ProviderCount} provider schedule(s).", schedules.Count);
var tasks = new List<Task>(schedules.Count);
foreach (var schedule in schedules)
{
tasks.Add(RunScheduleAsync(schedule, stoppingToken));
}
await Task.WhenAll(tasks);
}
private async Task RunScheduleAsync(VexWorkerSchedule schedule, CancellationToken cancellationToken)
{
try
{
if (schedule.InitialDelay > TimeSpan.Zero)
{
_logger.LogInformation(
"Provider {ProviderId} initial delay of {InitialDelay} before first execution.",
schedule.ProviderId,
schedule.InitialDelay);
await Task.Delay(schedule.InitialDelay, cancellationToken).ConfigureAwait(false);
}
using var timer = new PeriodicTimer(schedule.Interval);
do
{
var startedAt = _timeProvider.GetUtcNow();
_logger.LogInformation(
"Provider {ProviderId} run started at {StartedAt}. Interval={Interval}.",
schedule.ProviderId,
startedAt,
schedule.Interval);
try
{
await _runner.RunAsync(schedule.ProviderId, cancellationToken).ConfigureAwait(false);
var completedAt = _timeProvider.GetUtcNow();
var elapsed = completedAt - startedAt;
_logger.LogInformation(
"Provider {ProviderId} run completed at {CompletedAt} (duration {Duration}).",
schedule.ProviderId,
completedAt,
elapsed);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Provider {ProviderId} run cancelled.", schedule.ProviderId);
break;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Provider {ProviderId} run failed: {Message}",
schedule.ProviderId,
ex.Message);
}
}
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false));
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Provider {ProviderId} schedule cancelled.", schedule.ProviderId);
}
}
}