using Api.SeaHavenIndustries.Observability; using Microsoft.Extensions.Options; using SeaHaven.Services.Configuration; using SeaHaven.Services.Interfaces; using Sentry; namespace Api.SeaHavenIndustries.HostedServices { public sealed class WorkOrderReconciliationHostedService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly IOptionsMonitor _options; private readonly TimeProvider _timeProvider; private readonly ILogger _logger; private readonly IHub _sentryHub; public WorkOrderReconciliationHostedService( IServiceScopeFactory scopeFactory, IOptionsMonitor options, TimeProvider timeProvider, ILogger logger, IHub sentryHub) { _scopeFactory = scopeFactory; _options = options; _timeProvider = timeProvider; _logger = logger; _sentryHub = sentryHub; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var nextScheduledAt = _timeProvider.GetUtcNow(); if (_options.CurrentValue.Enabled && _options.CurrentValue.RunOnStartup) { await TriggerAsync("startup", stoppingToken); nextScheduledAt = _timeProvider.GetUtcNow().AddMinutes(_options.CurrentValue.ScheduleMinutes); } while (!stoppingToken.IsCancellationRequested) { var options = _options.CurrentValue; if (options.Enabled) { var now = _timeProvider.GetUtcNow(); if (now >= nextScheduledAt) { await TriggerAsync("schedule", stoppingToken); nextScheduledAt = now.AddMinutes(options.ScheduleMinutes); } await RunPendingAsync(stoppingToken); } await Task.Delay( TimeSpan.FromSeconds(Math.Clamp(options.PollSeconds, 1, 300)), _timeProvider, stoppingToken); } } private async Task TriggerAsync(string reason, CancellationToken cancellationToken) { using var transaction = SentryObservability.BeginBackgroundTransaction( _sentryHub, "workorders.reconciliation-trigger", $"{nameof(WorkOrderReconciliationHostedService)}.{nameof(TriggerAsync)}", description: reason); try { await using var scope = _scopeFactory.CreateAsyncScope(); var runner = scope.ServiceProvider.GetRequiredService(); await runner.TriggerAsync(reason, cancellationToken); transaction.FinishOk(); } catch (OperationCanceledException) { transaction.FinishCancelled(); throw; } catch (Exception ex) when (ex is not OperationCanceledException) { _logger.LogError(ex, "Could not queue procurement reconciliation."); transaction.FinishError(ex); } } private async Task RunPendingAsync(CancellationToken cancellationToken) { using var transaction = SentryObservability.BeginBackgroundTransaction( _sentryHub, "workorders.reconciliation-run-pending", $"{nameof(WorkOrderReconciliationHostedService)}.{nameof(RunPendingAsync)}"); try { await using var scope = _scopeFactory.CreateAsyncScope(); var runner = scope.ServiceProvider.GetRequiredService(); await runner.RunPendingAsync(cancellationToken); transaction.FinishOk(); } catch (OperationCanceledException) { transaction.FinishCancelled(); throw; } catch (Exception ex) when (ex is not OperationCanceledException) { _logger.LogError(ex, "Could not execute procurement reconciliation."); transaction.FinishError(ex); } } } }