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.Configuration.Model.GeneralSettings; using Mirea.Api.Endpoint.Sync; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Mirea.Api.Endpoint.Configuration.Core.BackgroundTasks; public class ScheduleSyncService : IHostedService, IDisposable { private Timer? _timer; private string _cronUpdate; private List _cronUpdateSkip; private readonly ILogger _logger; private CancellationTokenSource _cancellationTokenSource = new(); private readonly IServiceProvider _serviceProvider; private readonly IDisposable? _onChangeUpdateCron; public ScheduleSyncService(IOptionsMonitor generalConfigMonitor, ILogger logger, IServiceProvider serviceProvider) { _logger = logger; _serviceProvider = serviceProvider; _cronUpdate = generalConfigMonitor.CurrentValue.ScheduleSettings!.CronUpdateSchedule; _cronUpdateSkip = generalConfigMonitor.CurrentValue.ScheduleSettings!.CronUpdateSkipDateList; ScheduleSyncManager.OnForceSyncRequested += OnForceSyncRequested; _onChangeUpdateCron = generalConfigMonitor.OnChange((config) => { var updated = false; if (config.ScheduleSettings?.CronUpdateSchedule != null && _cronUpdate != config.ScheduleSettings.CronUpdateSchedule) { _cronUpdate = config.ScheduleSettings.CronUpdateSchedule; updated = true; } if (config.ScheduleSettings?.CronUpdateSkipDateList != null && !config.ScheduleSettings.CronUpdateSkipDateList.SequenceEqual(_cronUpdateSkip)) { _cronUpdateSkip = config.ScheduleSettings.CronUpdateSkipDateList .OrderBy(x => x.End ?? x.Date) .ToList(); updated = true; } if (updated) OnUpdateIntervalRequested(); }); } private void OnForceSyncRequested() { _logger.LogInformation("It was requested to synchronize the data immediately."); StopAsync(CancellationToken.None).ContinueWith(_ => { _cancellationTokenSource = new CancellationTokenSource(); ExecuteTask(null); }); } private void OnUpdateIntervalRequested() { _logger.LogInformation("It was requested to update the time interval immediately."); StopAsync(CancellationToken.None).ContinueWith(_ => { StartAsync(CancellationToken.None); }); } private void ScheduleNextRun() { if (string.IsNullOrEmpty(_cronUpdate)) { _logger.LogWarning("Cron expression is not set. The scheduled task will not run."); return; } var expression = CronExpression.Parse(_cronUpdate); var nextRunTime = _cronUpdateSkip.GetNextTask(expression).FirstOrDefault(); if (nextRunTime == default) { _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.ToString("G")); var delay = (nextRunTime - 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, delay > int.MaxValue ? int.MaxValue : (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(CancellationToken.None).GetAwaiter().GetResult(); _timer?.Dispose(); ScheduleSyncManager.OnForceSyncRequested -= OnForceSyncRequested; _onChangeUpdateCron?.Dispose(); _cancellationTokenSource.Dispose(); GC.SuppressFinalize(this); } }