using FamilyNido.Api.Options; using Microsoft.Extensions.Options; namespace FamilyNido.Api.Features.Calendar; /// /// Hosted service that runs on a /// fixed cadence (). Sleeps with a /// so the loop wakes up promptly on shutdown. /// public sealed class CalendarSyncBackgroundService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly IOptionsMonitor _options; private readonly ILogger _logger; /// Primary constructor. public CalendarSyncBackgroundService( IServiceScopeFactory scopeFactory, IOptionsMonitor options, ILogger logger) { _scopeFactory = scopeFactory; _options = options; _logger = logger; } /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var initialDelay = TimeSpan.FromSeconds(30); try { // Don't pile work onto a cold-starting app. A short head-start means the API is // already serving traffic when the first sync runs. await Task.Delay(initialDelay, stoppingToken); } catch (OperationCanceledException) { return; } while (!stoppingToken.IsCancellationRequested) { await SyncOnceAsync(stoppingToken); var interval = _options.CurrentValue.SyncInterval; if (interval <= TimeSpan.Zero) { interval = TimeSpan.FromMinutes(15); } try { await Task.Delay(interval, stoppingToken); } catch (OperationCanceledException) { return; } } } private async Task SyncOnceAsync(CancellationToken stoppingToken) { try { await using var scope = _scopeFactory.CreateAsyncScope(); var synchronizer = scope.ServiceProvider.GetRequiredService(); await synchronizer.SyncAllAsync(stoppingToken); } catch (OperationCanceledException) { // Shutting down; do not log. } catch (Exception ex) { _logger.LogError(ex, "Calendar sync iteration failed at the top level."); } } }