183 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			183 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using Cronos;
 | |
| using Mercados.Infrastructure.DataFetchers;
 | |
| using Mercados.Infrastructure.Services;
 | |
| using Microsoft.Extensions.Configuration;
 | |
| 
 | |
| 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;
 | |
| 
 | |
|         // Almacenamos las expresiones Cron parseadas para no tener que hacerlo en cada ciclo.
 | |
|         private readonly CronExpression _agroSchedule;
 | |
|         private readonly CronExpression _bcrSchedule;
 | |
|         private readonly CronExpression _bolsasSchedule;
 | |
| 
 | |
|         // Almacenamos la próxima ejecución calculada para cada tarea.
 | |
|         private DateTime? _nextAgroRun;
 | |
|         private DateTime? _nextBcrRun;
 | |
|         private DateTime? _nextBolsasRun;
 | |
| 
 | |
|         // Diccionario para rastrear la hora de la última alerta ENVIADA por cada tarea.
 | |
|         private readonly Dictionary<string, DateTime> _lastAlertSent = new();
 | |
|         // Definimos el período de "silencio" para las alertas (ej. 4 horas).
 | |
|         private readonly TimeSpan _alertSilencePeriod = TimeSpan.FromHours(4);
 | |
| 
 | |
|         public DataFetchingService(
 | |
|             ILogger<DataFetchingService> logger,
 | |
|             IServiceProvider serviceProvider,
 | |
|             IConfiguration configuration)
 | |
|         {
 | |
|             _logger = logger;
 | |
|             _serviceProvider = serviceProvider;
 | |
| 
 | |
|             // Se define explícitamente la zona horaria de Argentina.
 | |
|             try
 | |
|             {
 | |
|                 _argentinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("America/Argentina/Buenos_Aires");
 | |
|             }
 | |
|             catch (TimeZoneNotFoundException)
 | |
|             {
 | |
|                 _argentinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Argentina Standard Time");
 | |
|             }
 | |
| 
 | |
|             // Parseamos las expresiones Cron UNA SOLA VEZ, en el constructor.
 | |
|             // Si una expresión es inválida o nula, el servicio fallará al iniciar, 
 | |
|             // lo cual es un comportamiento deseable para alertar de una mala configuración.
 | |
|             // El '!' le dice al compilador que confiamos que estos valores no serán nulos.
 | |
|             _agroSchedule = CronExpression.Parse(configuration["Schedules:MercadoAgroganadero"]!);
 | |
|             _bcrSchedule = CronExpression.Parse(configuration["Schedules:BCR"]!);
 | |
|             _bolsasSchedule = CronExpression.Parse(configuration["Schedules:Bolsas"]!);
 | |
|         }
 | |
| 
 | |
|         /// <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(stoppingToken);
 | |
| 
 | |
|             // Calculamos las primeras ejecuciones programadas al arrancar.
 | |
|             var utcNow = DateTime.UtcNow;
 | |
|             _nextAgroRun = _agroSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone);
 | |
|             _nextBcrRun = _bcrSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone);
 | |
|             _nextBolsasRun = _bolsasSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone);
 | |
| 
 | |
|             // Usamos un PeriodicTimer que "despierta" cada 30 segundos para revisar si hay tareas pendientes.
 | |
|             // Un intervalo más corto aumenta la precisión del disparo de las tareas.
 | |
|             using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
 | |
| 
 | |
|             while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
 | |
|             {
 | |
|                 utcNow = DateTime.UtcNow;
 | |
| 
 | |
|                 // Comprobamos si ha llegado el momento de la próxima ejecución para cada tarea.
 | |
|                 if (_nextAgroRun.HasValue && utcNow >= _nextAgroRun.Value)
 | |
|                 {
 | |
|                     await RunFetcherByNameAsync("MercadoAgroganadero", stoppingToken);
 | |
|                     // Inmediatamente después de ejecutar, calculamos la SIGUIENTE ocurrencia.
 | |
|                     _nextAgroRun = _agroSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone);
 | |
|                 }
 | |
| 
 | |
|                 if (_nextBcrRun.HasValue && utcNow >= _nextBcrRun.Value)
 | |
|                 {
 | |
|                     await RunFetcherByNameAsync("BCR", stoppingToken);
 | |
|                     _nextBcrRun = _bcrSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone);
 | |
|                 }
 | |
| 
 | |
|                 if (_nextBolsasRun.HasValue && utcNow >= _nextBolsasRun.Value)
 | |
|                 {
 | |
|                     _logger.LogInformation("Ventana de ejecución para Bolsas. Iniciando en paralelo...");
 | |
|                     await Task.WhenAll(
 | |
|                         RunFetcherByNameAsync("YahooFinance", stoppingToken),
 | |
|                         RunFetcherByNameAsync("Finnhub", stoppingToken)
 | |
|                     );
 | |
|                     _nextBolsasRun = _bolsasSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Ejecuta un fetcher específico por su nombre, gestionando el scope de DI y las notificaciones.
 | |
|         /// </summary>
 | |
|         private async Task RunFetcherByNameAsync(string sourceName, CancellationToken stoppingToken)
 | |
|         {
 | |
|             if (stoppingToken.IsCancellationRequested) return;
 | |
| 
 | |
|             _logger.LogInformation("Intentando ejecutar fetcher: {sourceName}", sourceName);
 | |
| 
 | |
|             using var scope = _serviceProvider.CreateScope();
 | |
|             var fetchers = scope.ServiceProvider.GetRequiredService<IEnumerable<IDataFetcher>>();
 | |
|             var fetcher = fetchers.FirstOrDefault(f => f.SourceName.Equals(sourceName, StringComparison.OrdinalIgnoreCase));
 | |
| 
 | |
|             if (fetcher != null)
 | |
|             {
 | |
|                 var (success, message) = await fetcher.FetchDataAsync();
 | |
|                 if (!success)
 | |
|                 {
 | |
|                     var errorMessage = $"Falló la ejecución del fetcher {sourceName}: {message}";
 | |
|                     _logger.LogError(errorMessage);
 | |
| 
 | |
|                     if (ShouldSendAlert(sourceName))
 | |
|                     {
 | |
|                         var notifier = scope.ServiceProvider.GetRequiredService<INotificationService>();
 | |
|                         await notifier.SendFailureAlertAsync($"Fallo Crítico en el Fetcher: {sourceName}", errorMessage, DateTime.UtcNow);
 | |
|                         _lastAlertSent[sourceName] = DateTime.UtcNow;
 | |
|                     }
 | |
|                     else
 | |
|                     {
 | |
|                         _logger.LogWarning("Fallo repetido para {sourceName}. Alerta silenciada temporalmente.", sourceName);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 _logger.LogWarning("No se encontró un fetcher con el nombre: {sourceName}", sourceName);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Ejecuta todos los fetchers en paralelo al iniciar el servicio.
 | |
|         /// </summary>
 | |
|         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<IEnumerable<IDataFetcher>>();
 | |
| 
 | |
|             var tasks = fetchers.Select(fetcher => RunFetcherByNameAsync(fetcher.SourceName, stoppingToken));
 | |
| 
 | |
|             await Task.WhenAll(tasks);
 | |
| 
 | |
|             _logger.LogInformation("Ejecución inicial de todos los fetchers completada.");
 | |
|         }
 | |
| 
 | |
|         #region Funciones de Ayuda para la Planificación
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Determina si se debe enviar una alerta o si está en período de silencio.
 | |
|         /// </summary>
 | |
|         private bool ShouldSendAlert(string taskName)
 | |
|         {
 | |
|             if (!_lastAlertSent.ContainsKey(taskName))
 | |
|             {
 | |
|                 return true;
 | |
|             }
 | |
| 
 | |
|             var lastAlertTime = _lastAlertSent[taskName];
 | |
|             return DateTime.UtcNow.Subtract(lastAlertTime) > _alertSilencePeriod;
 | |
|         }
 | |
| 
 | |
|         #endregion
 | |
|     }
 | |
| } |