using Mercados.Infrastructure.DataFetchers; using Cronos; namespace Mercados.Worker { /// /// Servicio de fondo que orquesta la obtención de datos de diversas fuentes /// de forma programada y periódica. /// public class DataFetchingService : BackgroundService { private readonly ILogger _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 _lastDailyRun = new(); public DataFetchingService(ILogger 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 // 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"); } } /// /// Método principal del servicio. Se ejecuta una vez cuando el servicio arranca. /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("🚀 Servicio de Fetching iniciado a las: {time}", DateTimeOffset.Now); // 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); // 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)); // 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(stoppingToken); } } /// /// Revisa la hora actual y ejecuta las tareas que coincidan con su horario programado. /// private async Task RunScheduledTasksAsync(CancellationToken stoppingToken) { var utcNow = DateTime.UtcNow; // Tareas diarias (estas suelen ser rápidas y no se solapan, no es crítico paralelizar) // Mantenerlas secuenciales puede ser más simple de leer. string? agroSchedule = _configuration["Schedules:MercadoAgroganadero"]; if (!string.IsNullOrEmpty(agroSchedule)) { await TryRunDailyTaskAsync("MercadoAgroganadero", agroSchedule, utcNow, stoppingToken); } else { _logger.LogWarning("..."); } string? bcrSchedule = _configuration["Schedules:BCR"]; if (!string.IsNullOrEmpty(bcrSchedule)) { await TryRunDailyTaskAsync("BCR", bcrSchedule, utcNow, stoppingToken); } else { _logger.LogWarning("..."); } // --- Tareas Recurrentes (Bolsas) --- string? bolsasSchedule = _configuration["Schedules:Bolsas"]; if (!string.IsNullOrEmpty(bolsasSchedule)) { // Reemplazamos la llamada secuencial con la ejecución paralela await TryRunRecurringTaskInParallelAsync(new[] { "YahooFinance", "Finnhub" }, bolsasSchedule, utcNow, stoppingToken); } else { _logger.LogWarning("..."); } } /// /// Comprueba y ejecuta una tarea que debe correr solo una vez al día. /// 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; } } } /// /// Comprueba y ejecuta una tarea que puede correr múltiples veces al día. /// private async Task TryRunRecurringTaskInParallelAsync(string[] taskNames, string cronExpression, DateTime utcNow, CancellationToken stoppingToken) { var cron = CronExpression.Parse(cronExpression, CronFormat.IncludeSeconds); var nextOccurrence = cron.GetNextOccurrence(utcNow.AddMinutes(-1)); if (nextOccurrence.HasValue && nextOccurrence.Value <= utcNow) { _logger.LogInformation("Ventana de ejecución para: {Tasks}. Iniciando en paralelo...", string.Join(", ", taskNames)); // Creamos una lista de tareas, una por cada fetcher a ejecutar var tasks = taskNames.Select(taskName => RunFetcherByNameAsync(taskName, stoppingToken)).ToList(); // Iniciamos todas las tareas a la vez y esperamos a que todas terminen await Task.WhenAll(tasks); _logger.LogInformation("Todas las tareas recurrentes han finalizado."); } } /// /// 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). /// 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>(); var fetcher = fetchers.FirstOrDefault(f => f.SourceName.Equals(sourceName, StringComparison.OrdinalIgnoreCase)); if (fetcher != null) { var (success, message) = await fetcher.FetchDataAsync(); if (!success) { _logger.LogError("Falló la ejecución del fetcher {sourceName}: {message}", sourceName, message); } } else { _logger.LogWarning("No se encontró un fetcher con el nombre: {sourceName}", sourceName); } } /// /// Ejecuta todos los fetchers al iniciar el servicio. Esto es útil para poblar /// la base de datos inmediatamente al arrancar el worker. /// /* private async Task RunAllFetchersAsync(CancellationToken stoppingToken) { _logger.LogInformation("Ejecutando todos los fetchers al iniciar en paralelo..."); using var scope = _serviceProvider.CreateScope(); var fetchers = scope.ServiceProvider.GetRequiredService>(); // Creamos una lista de tareas, una por cada fetcher disponible var tasks = fetchers.Select(fetcher => RunFetcherByNameAsync(fetcher.SourceName, stoppingToken)).ToList(); // Ejecutamos todo y esperamos await Task.WhenAll(tasks); _logger.LogInformation("Ejecución inicial de todos los fetchers completada."); } */ #region Funciones de Ayuda para la Planificación private bool HasNotRunToday(string taskName) { return !_lastDailyRun.ContainsKey(taskName) || _lastDailyRun[taskName].Date < TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone).Date; } #endregion } }