diff --git a/Endpoint/Common/Services/ScheduleSyncManager.cs b/Endpoint/Common/Services/ScheduleSyncManager.cs new file mode 100644 index 0000000..7bff018 --- /dev/null +++ b/Endpoint/Common/Services/ScheduleSyncManager.cs @@ -0,0 +1,15 @@ +using System; + +namespace Mirea.Api.Endpoint.Common.Services; + +public static class ScheduleSyncManager +{ + public static event Action? OnUpdateIntervalRequested; + public static event Action? OnForceSyncRequested; + + public static void RequestIntervalUpdate() => + OnUpdateIntervalRequested?.Invoke(); + + public static void RequestForceSync() => + OnForceSyncRequested?.Invoke(); +} \ No newline at end of file diff --git a/Endpoint/Configuration/Core/BackgroundTasks/ScheduleSyncService.cs b/Endpoint/Configuration/Core/BackgroundTasks/ScheduleSyncService.cs new file mode 100644 index 0000000..6186f3e --- /dev/null +++ b/Endpoint/Configuration/Core/BackgroundTasks/ScheduleSyncService.cs @@ -0,0 +1,121 @@ +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; +using IServiceProvider = System.IServiceProvider; + +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); + } +} \ No newline at end of file diff --git a/Endpoint/Endpoint.csproj b/Endpoint/Endpoint.csproj index 5e86f4e..da40f60 100644 --- a/Endpoint/Endpoint.csproj +++ b/Endpoint/Endpoint.csproj @@ -5,9 +5,9 @@ disable enable Winsomnia - 1.0-rc3 - 1.0.2.3 - 1.0.2.3 + 1.0-rc4 + 1.0.2.4 + 1.0.2.4 Mirea.Api.Endpoint $(AssemblyName) Exe diff --git a/Endpoint/Program.cs b/Endpoint/Program.cs index 5d16dc2..4ce387a 100644 --- a/Endpoint/Program.cs +++ b/Endpoint/Program.cs @@ -10,6 +10,7 @@ using Mirea.Api.DataAccess.Persistence; using Mirea.Api.DataAccess.Persistence.Common; using Mirea.Api.Endpoint.Common.Interfaces; using Mirea.Api.Endpoint.Common.Services; +using Mirea.Api.Endpoint.Configuration.Core.BackgroundTasks; using Mirea.Api.Endpoint.Configuration.Core.Middleware; using Mirea.Api.Endpoint.Configuration.Core.Startup; using Mirea.Api.Endpoint.Configuration.Model; @@ -63,6 +64,8 @@ public class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddHostedService(); + builder.Services.AddMemoryCache(); builder.Services.AddCustomRedis(builder.Configuration, healthCheckBuilder);