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 _options; private readonly IVexProviderRunner _runner; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public VexWorkerHostedService( IOptions options, IVexProviderRunner runner, ILogger 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(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); } } }