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 string _cronUpdate; 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; ScheduleSyncManager.OnForceSyncRequested += OnForceSyncRequested; _onChangeUpdateCron = generalConfigMonitor.OnChange((config) => { if (config.ScheduleSettings?.CronUpdateSchedule == null || _cronUpdate == config.ScheduleSettings.CronUpdateSchedule) return; _cronUpdate = config.ScheduleSettings.CronUpdateSchedule; 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 nextRunTime = CronExpression.Parse(_cronUpdate).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, Math.Abs((long)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); } }