using Cronos; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Mirea.Api.Endpoint.Common.Services; using Mirea.Api.Endpoint.Configuration.Model; using Mirea.Api.Endpoint.Sync; using System; using System.Threading; using System.Threading.Tasks; namespace Mirea.Api.Endpoint.Configuration.Core.BackgroundTasks; public class ScheduleSyncService : IHostedService, IDisposable { private Timer? _timer; private readonly IOptionsMonitor _generalConfigMonitor; private readonly ILogger _logger; private CancellationTokenSource _cancellationTokenSource = new(); private readonly IServiceProvider _serviceProvider; public ScheduleSyncService(IOptionsMonitor generalConfigMonitor, ILogger logger, IServiceProvider serviceProvider) { _generalConfigMonitor = generalConfigMonitor; _logger = logger; _serviceProvider = serviceProvider; ScheduleSyncManager.OnForceSyncRequested += OnForceSyncRequested; ScheduleSyncManager.OnUpdateIntervalRequested += OnUpdateIntervalRequested; } private void OnForceSyncRequested() { StopAsync(default).ContinueWith(_ => { _cancellationTokenSource = new CancellationTokenSource(); ExecuteTask(null); }); } private void OnUpdateIntervalRequested() { StopAsync(default).ContinueWith(_ => { StartAsync(default); }); } private void ScheduleNextRun() { var cronExpression = _generalConfigMonitor.CurrentValue.ScheduleSettings?.CronUpdateSchedule; if (string.IsNullOrEmpty(cronExpression)) { _logger.LogWarning("Cron expression is not set. The scheduled task will not run."); return; } var nextRunTime = CronExpression.Parse(cronExpression).GetNextOccurrence(DateTimeOffset.Now, TimeZoneInfo.Local); if (!nextRunTime.HasValue) { _logger.LogWarning("No next run time found. The task will not be scheduled. Timezone: {TimeZone}", TimeZoneInfo.Local.DisplayName); return; } _logger.LogInformation("Next task run in {Time}", nextRunTime.Value.ToString("G")); var delay = (nextRunTime.Value - DateTimeOffset.Now).TotalMilliseconds; // The chance is small, but it's better to check if (delay <= 0) delay = 1; _cancellationTokenSource = new CancellationTokenSource(); _timer = new Timer(ExecuteTask, null, (int)delay, Timeout.Infinite); } private async void ExecuteTask(object? state) { try { using var scope = _serviceProvider.CreateScope(); var syncService = ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider); await syncService.StartSync(_cancellationTokenSource.Token); } catch (Exception ex) { _logger.LogError(ex, "Error occurred during schedule synchronization."); } finally { ScheduleNextRun(); } } public Task StartAsync(CancellationToken cancellationToken) { ScheduleNextRun(); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { _cancellationTokenSource.Cancel(); _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public void Dispose() { StopAsync(default).GetAwaiter().GetResult(); _timer?.Dispose(); ScheduleSyncManager.OnForceSyncRequested -= OnForceSyncRequested; ScheduleSyncManager.OnUpdateIntervalRequested -= OnUpdateIntervalRequested; _cancellationTokenSource.Dispose(); GC.SuppressFinalize(this); } }