feat: adaptación de los proyectos para utilizar .env y comienzo de preparación para despliegue en docker
This commit is contained in:
		| @@ -2,70 +2,109 @@ using Mercados.Infrastructure.DataFetchers; | ||||
|  | ||||
| namespace Mercados.Worker | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Servicio de fondo que orquesta la obtención de datos de diversas fuentes | ||||
|     /// de forma programada y periódica. | ||||
|     /// </summary> | ||||
|     public class DataFetchingService : BackgroundService | ||||
|     { | ||||
|         private readonly ILogger<DataFetchingService> _logger; | ||||
|         private readonly IServiceProvider _serviceProvider; | ||||
|         private readonly TimeZoneInfo _argentinaTimeZone; | ||||
|  | ||||
|         // 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. | ||||
|         private readonly Dictionary<string, DateTime> _lastDailyRun = new(); | ||||
|  | ||||
|         public DataFetchingService(ILogger<DataFetchingService> logger, IServiceProvider serviceProvider) | ||||
|         { | ||||
|             _logger = logger; | ||||
|             _serviceProvider = serviceProvider; | ||||
|              | ||||
|             // Se define explícitamente la zona horaria de Argentina. | ||||
|             // Esto asegura que los cálculos de tiempo sean correctos, sin importar | ||||
|             // la configuración de zona horaria del servidor donde se ejecute el worker. | ||||
|             try | ||||
|             { | ||||
|                 // El ID estándar para Linux y macOS | ||||
|                 _argentinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("America/Argentina/Buenos_Aires"); | ||||
|             } | ||||
|             catch (TimeZoneNotFoundException) | ||||
|             { | ||||
|                 // El ID equivalente para Windows | ||||
|                 _argentinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Argentina Standard Time"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Método principal del servicio. Se ejecuta una vez cuando el servicio arranca. | ||||
|         /// </summary> | ||||
|         protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|         { | ||||
|             _logger.LogInformation("🚀 Servicio de Fetching iniciado a las: {time}", DateTimeOffset.Now); | ||||
|  | ||||
|             // Ejecutamos una vez al inicio para tener datos frescos inmediatamente. | ||||
|             await RunAllFetchersAsync(); | ||||
|             // Se recomienda una ejecución inicial para poblar la base de datos inmediatamente | ||||
|             // al iniciar el servicio, en lugar de esperar al primer horario programado. | ||||
|             //await RunAllFetchersAsync(stoppingToken); | ||||
|  | ||||
|             // Usamos un PeriodicTimer que "despierta" cada minuto para revisar si hay tareas pendientes. | ||||
|             // PeriodicTimer es una forma moderna y eficiente de crear un bucle de "tic-tac" | ||||
|             // sin bloquear un hilo con Task.Delay. | ||||
|             using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1)); | ||||
|  | ||||
|             while (await timer.WaitForNextTickAsync(stoppingToken)) | ||||
|             // El bucle se ejecuta cada minuto mientras el servicio no reciba una señal de detención. | ||||
|             while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) | ||||
|             { | ||||
|                 await RunScheduledTasksAsync(); | ||||
|                 await RunScheduledTasksAsync(stoppingToken); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private async Task RunScheduledTasksAsync() | ||||
|         /// <summary> | ||||
|         /// Revisa la hora actual y ejecuta las tareas que coincidan con su horario programado. | ||||
|         /// </summary> | ||||
|         private async Task RunScheduledTasksAsync(CancellationToken stoppingToken) | ||||
|         { | ||||
|             // --- Lógica de Planificación --- | ||||
|             var now = DateTime.Now; | ||||
|             // Se obtiene la hora actual convertida a la zona horaria de Argentina. | ||||
|             var nowInArgentina = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone); | ||||
|  | ||||
|             // Tarea 1: Mercado Agroganadero (todos los días a las 11:00) | ||||
|             if (now.Hour == 11 && now.Minute == 0 && HasNotRunToday("MercadoAgroganadero")) | ||||
|             // --- Tarea 1: Mercado Agroganadero (L-V a las 11:00 AM) --- | ||||
|             if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 0 && HasNotRunToday("MercadoAgroganadero")) | ||||
|             { | ||||
|                 await RunFetcherByNameAsync("MercadoAgroganadero"); | ||||
|                 _lastDailyRun["MercadoAgroganadero"] = now.Date; | ||||
|                 await RunFetcherByNameAsync("MercadoAgroganadero", stoppingToken); | ||||
|                 _lastDailyRun["MercadoAgroganadero"] = nowInArgentina.Date; | ||||
|             } | ||||
|  | ||||
|             // Tarea 2: Granos BCR (todos los días a las 11:30) | ||||
|             if (now.Hour == 11 && now.Minute == 30 && HasNotRunToday("BCR")) | ||||
|             // --- Tarea 2: Granos BCR (L-V a las 11:30 AM) --- | ||||
|             if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 30 && HasNotRunToday("BCR")) | ||||
|             { | ||||
|                 await RunFetcherByNameAsync("BCR"); | ||||
|                 _lastDailyRun["BCR"] = now.Date; | ||||
|                 await RunFetcherByNameAsync("BCR", stoppingToken); | ||||
|                 _lastDailyRun["BCR"] = nowInArgentina.Date; | ||||
|             } | ||||
|  | ||||
|             // Tarea 3: Mercados de Bolsa (cada 10 minutos si el mercado está abierto) | ||||
|             if (IsMarketOpen(now) && now.Minute % 10 == 0) | ||||
|             // --- 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) | ||||
|             { | ||||
|                 _logger.LogInformation("Mercados abiertos. Ejecutando fetchers de bolsa."); | ||||
|                 await RunFetcherByNameAsync("Finnhub"); | ||||
|                 await RunFetcherByNameAsync("YahooFinance"); | ||||
|                 _logger.LogInformation("Hora de actualización de mercados de bolsa. Ejecutando fetchers..."); | ||||
|                  | ||||
|                 await RunFetcherByNameAsync("YahooFinance", stoppingToken); | ||||
|                 // Si Finnhub está desactivado en Program.cs, este simplemente no se encontrará y se omitirá. | ||||
|                 await RunFetcherByNameAsync("Finnhub", stoppingToken); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Esta función crea un "scope" para ejecutar un fetcher específico. | ||||
|         // Esto es crucial para que la inyección de dependencias funcione correctamente. | ||||
|         private async Task RunFetcherByNameAsync(string sourceName) | ||||
|         /// <summary> | ||||
|         /// Ejecuta un fetcher específico por su nombre. Utiliza un scope de DI para gestionar | ||||
|         /// correctamente el ciclo de vida de los servicios (como las conexiones a la BD). | ||||
|         /// </summary> | ||||
|         private async Task RunFetcherByNameAsync(string sourceName, CancellationToken stoppingToken) | ||||
|         { | ||||
|             if (stoppingToken.IsCancellationRequested) return; | ||||
|  | ||||
|             _logger.LogInformation("Intentando ejecutar fetcher: {sourceName}", sourceName); | ||||
|              | ||||
|             // Crea un "scope" de servicios. Todos los servicios "scoped" (como los repositorios) | ||||
|             // se crearán de nuevo para esta ejecución y se desecharán al final, evitando problemas. | ||||
|             using var scope = _serviceProvider.CreateScope(); | ||||
|             var fetchers = scope.ServiceProvider.GetRequiredService<IEnumerable<IDataFetcher>>(); | ||||
|             var fetcher = fetchers.FirstOrDefault(f => f.SourceName.Equals(sourceName, StringComparison.OrdinalIgnoreCase)); | ||||
| @@ -84,32 +123,42 @@ namespace Mercados.Worker | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Función de ayuda para ejecutar todos los fetchers (usada al inicio). | ||||
|         private async Task RunAllFetchersAsync() | ||||
|         /// <summary> | ||||
|         /// Ejecuta todos los fetchers al iniciar el servicio. Esto es útil para poblar | ||||
|         /// la base de datos inmediatamente al arrancar el worker. | ||||
|         /// </summary> | ||||
|         /* | ||||
|         private async Task RunAllFetchersAsync(CancellationToken stoppingToken) | ||||
|         { | ||||
|             _logger.LogInformation("Ejecutando todos los fetchers al iniciar..."); | ||||
|             using var scope = _serviceProvider.CreateScope(); | ||||
|             var fetchers = scope.ServiceProvider.GetRequiredService<IEnumerable<IDataFetcher>>(); | ||||
|             foreach (var fetcher in fetchers) | ||||
|             { | ||||
|                 await RunFetcherByNameAsync(fetcher.SourceName); | ||||
|                 if (stoppingToken.IsCancellationRequested) break; | ||||
|                 await RunFetcherByNameAsync(fetcher.SourceName, stoppingToken); | ||||
|             } | ||||
|         } | ||||
|         */ | ||||
|          | ||||
|         #region Funciones de Ayuda para la Planificación | ||||
|  | ||||
|         private bool HasNotRunToday(string taskName) | ||||
|         { | ||||
|             return !_lastDailyRun.ContainsKey(taskName) || _lastDailyRun[taskName].Date < DateTime.Now.Date; | ||||
|             // 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 IsMarketOpen(DateTime now) | ||||
|         private bool IsWeekDay(DateTime now) | ||||
|         { | ||||
|             // Lunes a Viernes (1 a 5, Domingo es 0) | ||||
|             if (now.DayOfWeek == DayOfWeek.Saturday || now.DayOfWeek == DayOfWeek.Sunday) | ||||
|                 return false; | ||||
|             return now.DayOfWeek >= DayOfWeek.Monday && now.DayOfWeek <= DayOfWeek.Friday; | ||||
|         } | ||||
|  | ||||
|         private bool IsArgentineMarketOpen(DateTime now) | ||||
|         { | ||||
|             if (!IsWeekDay(now)) return false; | ||||
|              | ||||
|             // Horario de mercado de 11:00 a 17:15 (hora de Argentina) | ||||
|             // 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); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user