feat(Worker): Refactoriza planificador para usar expresiones Cron desde configuración
This commit is contained in:
		| @@ -1,4 +1,5 @@ | |||||||
| using Mercados.Infrastructure.DataFetchers; | using Mercados.Infrastructure.DataFetchers; | ||||||
|  | using Cronos; | ||||||
|  |  | ||||||
| namespace Mercados.Worker | namespace Mercados.Worker | ||||||
| { | { | ||||||
| @@ -11,15 +12,17 @@ namespace Mercados.Worker | |||||||
|         private readonly ILogger<DataFetchingService> _logger; |         private readonly ILogger<DataFetchingService> _logger; | ||||||
|         private readonly IServiceProvider _serviceProvider; |         private readonly IServiceProvider _serviceProvider; | ||||||
|         private readonly TimeZoneInfo _argentinaTimeZone; |         private readonly TimeZoneInfo _argentinaTimeZone; | ||||||
|  |         private readonly IConfiguration _configuration; | ||||||
|  |  | ||||||
|         // Diccionario para rastrear la última vez que se ejecutó una tarea diaria |         // Diccionario para rastrear la última vez que se ejecutó una tarea diaria | ||||||
|         // y evitar que se ejecute múltiples veces si el servicio se reinicia. |         // y evitar que se ejecute múltiples veces si el servicio se reinicia. | ||||||
|         private readonly Dictionary<string, DateTime> _lastDailyRun = new(); |         private readonly Dictionary<string, DateTime> _lastDailyRun = new(); | ||||||
|  |  | ||||||
|         public DataFetchingService(ILogger<DataFetchingService> logger, IServiceProvider serviceProvider) |         public DataFetchingService(ILogger<DataFetchingService> logger, IServiceProvider serviceProvider,IConfiguration configuration) | ||||||
|         { |         { | ||||||
|             _logger = logger; |             _logger = logger; | ||||||
|             _serviceProvider = serviceProvider; |             _serviceProvider = serviceProvider; | ||||||
|  |             _configuration = configuration; | ||||||
|              |              | ||||||
|             // Se define explícitamente la zona horaria de Argentina. |             // Se define explícitamente la zona horaria de Argentina. | ||||||
|             // Esto asegura que los cálculos de tiempo sean correctos, sin importar |             // Esto asegura que los cálculos de tiempo sean correctos, sin importar | ||||||
| @@ -63,33 +66,78 @@ namespace Mercados.Worker | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         private async Task RunScheduledTasksAsync(CancellationToken stoppingToken) |         private async Task RunScheduledTasksAsync(CancellationToken stoppingToken) | ||||||
|         { |         { | ||||||
|             // Se obtiene la hora actual convertida a la zona horaria de Argentina. |             var utcNow = DateTime.UtcNow; | ||||||
|             var nowInArgentina = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone); |  | ||||||
|  |  | ||||||
|             // --- Tarea 1: Mercado Agroganadero (L-V a las 11:00 AM) --- |             // Obtenemos las expresiones Cron desde la configuración | ||||||
|             if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 0 && HasNotRunToday("MercadoAgroganadero")) |             string? agroSchedule = _configuration["Schedules:MercadoAgroganadero"]; | ||||||
|  |             string? bcrSchedule = _configuration["Schedules:BCR"]; | ||||||
|  |             string? bolsasSchedule = _configuration["Schedules:Bolsas"]; | ||||||
|  |  | ||||||
|  |             // Comprobamos cada una antes de usarla | ||||||
|  |             if (!string.IsNullOrEmpty(agroSchedule)) | ||||||
|             { |             { | ||||||
|                 await RunFetcherByNameAsync("MercadoAgroganadero", stoppingToken); |                 await TryRunDailyTaskAsync("MercadoAgroganadero", agroSchedule, utcNow, stoppingToken); | ||||||
|                 _lastDailyRun["MercadoAgroganadero"] = nowInArgentina.Date; |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 _logger.LogWarning("No se encontró la configuración de horario para 'MercadoAgroganadero' en appsettings.json."); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // --- Tarea 2: Granos BCR (L-V a las 11:30 AM) --- |             if (!string.IsNullOrEmpty(bcrSchedule)) | ||||||
|             if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 30 && HasNotRunToday("BCR")) |  | ||||||
|             { |             { | ||||||
|                 await RunFetcherByNameAsync("BCR", stoppingToken); |                 await TryRunDailyTaskAsync("BCR", bcrSchedule, utcNow, stoppingToken); | ||||||
|                 _lastDailyRun["BCR"] = nowInArgentina.Date; |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 _logger.LogWarning("No se encontró la configuración de horario para 'BCR' en appsettings.json."); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // --- Tarea 3 y 4: Mercados de Bolsa (L-V, durante horario de mercado, una vez por hora) --- |             if (!string.IsNullOrEmpty(bolsasSchedule)) | ||||||
|             // Se ejecutan si el mercado está abierto y si el minuto actual es exactamente 10. |  | ||||||
|             // Esto replica la lógica de "cada hora a las y 10". |  | ||||||
|             if (IsArgentineMarketOpen(nowInArgentina) && nowInArgentina.Minute == 10) |  | ||||||
|             { |             { | ||||||
|                 _logger.LogInformation("Hora de actualización de mercados de bolsa. Ejecutando fetchers..."); |                 await TryRunRecurringTaskAsync(new[] { "YahooFinance", "Finnhub" }, bolsasSchedule, utcNow, stoppingToken); | ||||||
|                  |             } | ||||||
|                 await RunFetcherByNameAsync("YahooFinance", stoppingToken); |             else | ||||||
|                 // Si Finnhub está desactivado en Program.cs, este simplemente no se encontrará y se omitirá. |             { | ||||||
|                 await RunFetcherByNameAsync("Finnhub", stoppingToken); |                 _logger.LogWarning("No se encontró la configuración de horario para 'Bolsas' en appsettings.json."); | ||||||
|  |             } | ||||||
|  |             // --- ^ FIN DE LA CORRECCIÓN DE NULABILIDAD ^ --- | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Comprueba y ejecuta una tarea que debe correr solo una vez al día. | ||||||
|  |         /// </summary> | ||||||
|  |         private async Task TryRunDailyTaskAsync(string taskName, string cronExpression, DateTime utcNow, CancellationToken stoppingToken) | ||||||
|  |         { | ||||||
|  |             var cron = CronExpression.Parse(cronExpression); | ||||||
|  |             var nextOccurrence = cron.GetNextOccurrence(utcNow.AddMinutes(-1)); | ||||||
|  |  | ||||||
|  |             if (nextOccurrence.HasValue && nextOccurrence.Value <= utcNow) | ||||||
|  |             { | ||||||
|  |                 if (HasNotRunToday(taskName)) | ||||||
|  |                 { | ||||||
|  |                     await RunFetcherByNameAsync(taskName, stoppingToken); | ||||||
|  |                     _lastDailyRun[taskName] = TimeZoneInfo.ConvertTimeFromUtc(utcNow, _argentinaTimeZone).Date; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Comprueba y ejecuta una tarea que puede correr múltiples veces al día. | ||||||
|  |         /// </summary> | ||||||
|  |         private async Task TryRunRecurringTaskAsync(string[] taskNames, string cronExpression, DateTime utcNow, CancellationToken stoppingToken) | ||||||
|  |         { | ||||||
|  |             // Añadimos 'IncludeSeconds' para que la comparación sea precisa y no se ejecute dos veces en el mismo minuto. | ||||||
|  |             var cron = CronExpression.Parse(cronExpression, CronFormat.IncludeSeconds); | ||||||
|  |             // Comprobamos si hubo una ocurrencia en el último minuto. | ||||||
|  |             var nextOccurrence = cron.GetNextOccurrence(utcNow.AddMinutes(-1)); | ||||||
|  |              | ||||||
|  |             if (nextOccurrence.HasValue && nextOccurrence.Value <= utcNow) | ||||||
|  |             { | ||||||
|  |                 _logger.LogInformation("Ventana de ejecución recurrente detectada para: {Tasks}", string.Join(", ", taskNames)); | ||||||
|  |                 foreach (var taskName in taskNames) | ||||||
|  |                 { | ||||||
|  |                     await RunFetcherByNameAsync(taskName, stoppingToken); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
| @@ -145,26 +193,9 @@ namespace Mercados.Worker | |||||||
|  |  | ||||||
|         private bool HasNotRunToday(string taskName) |         private bool HasNotRunToday(string taskName) | ||||||
|         { |         { | ||||||
|             // Comprueba si la tarea ya se ejecutó en la fecha actual (en zona horaria de Argentina). |  | ||||||
|             return !_lastDailyRun.ContainsKey(taskName) || _lastDailyRun[taskName].Date < TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone).Date; |             return !_lastDailyRun.ContainsKey(taskName) || _lastDailyRun[taskName].Date < TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone).Date; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         private bool IsWeekDay(DateTime now) |  | ||||||
|         { |  | ||||||
|             return now.DayOfWeek >= DayOfWeek.Monday && now.DayOfWeek <= DayOfWeek.Friday; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         private bool IsArgentineMarketOpen(DateTime now) |  | ||||||
|         { |  | ||||||
|             if (!IsWeekDay(now)) return false; |  | ||||||
|              |  | ||||||
|             // Rango de 11:00 a 17:15, para asegurar la captura del cierre a las 17:10. |  | ||||||
|             var marketOpen = new TimeSpan(11, 0, 0); |  | ||||||
|             var marketClose = new TimeSpan(17, 15, 0); |  | ||||||
|  |  | ||||||
|             return now.TimeOfDay >= marketOpen && now.TimeOfDay <= marketClose; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         #endregion |         #endregion | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -8,6 +8,7 @@ | |||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|  |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|  |     <PackageReference Include="Cronos" Version="0.11.0" /> | ||||||
|     <PackageReference Include="DotNetEnv" Version="3.1.1" /> |     <PackageReference Include="DotNetEnv" Version="3.1.1" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" /> |     <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|   | |||||||
| @@ -9,6 +9,11 @@ | |||||||
|   "ConnectionStrings": { |   "ConnectionStrings": { | ||||||
|     "DefaultConnection": "" |     "DefaultConnection": "" | ||||||
|   }, |   }, | ||||||
|  |   "Schedules": { | ||||||
|  |     "MercadoAgroganadero": "0 11 * * 1-5", | ||||||
|  |     "BCR": "30 11 * * 1-5", | ||||||
|  |     "Bolsas": "10 11-17 * * 1-5" | ||||||
|  |   }, | ||||||
|   "ApiKeys": { |   "ApiKeys": { | ||||||
|     "Finnhub": "", |     "Finnhub": "", | ||||||
|     "Bcr": { |     "Bcr": { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user