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 Cronos; | ||||
|  | ||||
| namespace Mercados.Worker | ||||
| { | ||||
| @@ -11,15 +12,17 @@ namespace Mercados.Worker | ||||
|         private readonly ILogger<DataFetchingService> _logger; | ||||
|         private readonly IServiceProvider _serviceProvider; | ||||
|         private readonly TimeZoneInfo _argentinaTimeZone; | ||||
|         private readonly IConfiguration _configuration; | ||||
|  | ||||
|         // 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. | ||||
|         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; | ||||
|             _serviceProvider = serviceProvider; | ||||
|             _configuration = configuration; | ||||
|              | ||||
|             // Se define explícitamente la zona horaria de Argentina. | ||||
|             // Esto asegura que los cálculos de tiempo sean correctos, sin importar | ||||
| @@ -63,33 +66,78 @@ namespace Mercados.Worker | ||||
|         /// </summary> | ||||
|         private async Task RunScheduledTasksAsync(CancellationToken stoppingToken) | ||||
|         { | ||||
|             // Se obtiene la hora actual convertida a la zona horaria de Argentina. | ||||
|             var nowInArgentina = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone); | ||||
|             var utcNow = DateTime.UtcNow; | ||||
|  | ||||
|             // --- Tarea 1: Mercado Agroganadero (L-V a las 11:00 AM) --- | ||||
|             if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 0 && HasNotRunToday("MercadoAgroganadero")) | ||||
|             // Obtenemos las expresiones Cron desde la configuración | ||||
|             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); | ||||
|                 _lastDailyRun["MercadoAgroganadero"] = nowInArgentina.Date; | ||||
|                 await TryRunDailyTaskAsync("MercadoAgroganadero", agroSchedule, utcNow, stoppingToken); | ||||
|             } | ||||
|             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 (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 30 && HasNotRunToday("BCR")) | ||||
|             if (!string.IsNullOrEmpty(bcrSchedule)) | ||||
|             { | ||||
|                 await RunFetcherByNameAsync("BCR", stoppingToken); | ||||
|                 _lastDailyRun["BCR"] = nowInArgentina.Date; | ||||
|                 await TryRunDailyTaskAsync("BCR", bcrSchedule, utcNow, stoppingToken); | ||||
|             } | ||||
|             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) --- | ||||
|             // 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) | ||||
|             if (!string.IsNullOrEmpty(bolsasSchedule)) | ||||
|             { | ||||
|                 _logger.LogInformation("Hora de actualización de mercados de bolsa. Ejecutando fetchers..."); | ||||
|                 await TryRunRecurringTaskAsync(new[] { "YahooFinance", "Finnhub" }, bolsasSchedule, utcNow, stoppingToken); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _logger.LogWarning("No se encontró la configuración de horario para 'Bolsas' en appsettings.json."); | ||||
|             } | ||||
|             // --- ^ FIN DE LA CORRECCIÓN DE NULABILIDAD ^ --- | ||||
|         } | ||||
|  | ||||
|                 await RunFetcherByNameAsync("YahooFinance", stoppingToken); | ||||
|                 // Si Finnhub está desactivado en Program.cs, este simplemente no se encontrará y se omitirá. | ||||
|                 await RunFetcherByNameAsync("Finnhub", stoppingToken); | ||||
|         /// <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) | ||||
|         { | ||||
|             // 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; | ||||
|         } | ||||
|  | ||||
|         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 | ||||
|     } | ||||
| } | ||||
| @@ -8,6 +8,7 @@ | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Cronos" Version="0.11.0" /> | ||||
|     <PackageReference Include="DotNetEnv" Version="3.1.1" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" /> | ||||
|   </ItemGroup> | ||||
|   | ||||
| @@ -9,6 +9,11 @@ | ||||
|   "ConnectionStrings": { | ||||
|     "DefaultConnection": "" | ||||
|   }, | ||||
|   "Schedules": { | ||||
|     "MercadoAgroganadero": "0 11 * * 1-5", | ||||
|     "BCR": "30 11 * * 1-5", | ||||
|     "Bolsas": "10 11-17 * * 1-5" | ||||
|   }, | ||||
|   "ApiKeys": { | ||||
|     "Finnhub": "", | ||||
|     "Bcr": { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user