diff --git a/src/Mercados.Infrastructure/DataFetchers/BcrDataFetcher.cs b/src/Mercados.Infrastructure/DataFetchers/BcrDataFetcher.cs index 7e435f6..6100fd0 100644 --- a/src/Mercados.Infrastructure/DataFetchers/BcrDataFetcher.cs +++ b/src/Mercados.Infrastructure/DataFetchers/BcrDataFetcher.cs @@ -69,7 +69,8 @@ namespace Mercados.Infrastructure.DataFetchers _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); try { - var client = _httpClientFactory.CreateClient(); // Creamos el cliente una vez + // Pedimos el cliente con el nombre correcto + var client = _httpClientFactory.CreateClient("BcrDataFetcher"); var token = await GetAuthTokenAsync(client); if (string.IsNullOrEmpty(token)) diff --git a/src/Mercados.Infrastructure/DataFetchers/FinnhubDataFetcher.cs b/src/Mercados.Infrastructure/DataFetchers/FinnhubDataFetcher.cs index 0a5b9f4..05dc889 100644 --- a/src/Mercados.Infrastructure/DataFetchers/FinnhubDataFetcher.cs +++ b/src/Mercados.Infrastructure/DataFetchers/FinnhubDataFetcher.cs @@ -18,7 +18,7 @@ namespace Mercados.Infrastructure.DataFetchers // ADRs Argentinos "YPF", "GGAL", "BMA", "LOMA", "PAM", "TEO", "TGS", "EDN", "CRESY", "CEPU", "BBAR" }; - + private readonly FinnhubClient _client; private readonly ICotizacionBolsaRepository _cotizacionRepository; private readonly IFuenteDatoRepository _fuenteDatoRepository; @@ -36,7 +36,8 @@ namespace Mercados.Infrastructure.DataFetchers { throw new InvalidOperationException("La clave de API de Finnhub no está configurada en appsettings.json (ApiKeys:Finnhub)"); } - _client = new FinnhubClient(httpClientFactory.CreateClient("Finnhub"), apiKey); + // Le pasamos el cliente HTTP que ya está configurado con Polly en Program.cs + _client = new FinnhubClient(httpClientFactory.CreateClient("FinnhubDataFetcher"), apiKey); _cotizacionRepository = cotizacionRepository; _fuenteDatoRepository = fuenteDatoRepository; _logger = logger; @@ -46,7 +47,7 @@ namespace Mercados.Infrastructure.DataFetchers { _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); var cotizaciones = new List(); - + foreach (var ticker in _tickers) { try @@ -56,7 +57,7 @@ namespace Mercados.Infrastructure.DataFetchers if (quote.Current == 0 || quote.PreviousClose == 0) continue; var pctChange = ((quote.Current - quote.PreviousClose) / quote.PreviousClose) * 100; - + cotizaciones.Add(new CotizacionBolsa { Ticker = ticker, @@ -79,11 +80,11 @@ namespace Mercados.Infrastructure.DataFetchers await _cotizacionRepository.GuardarMuchosAsync(cotizaciones); await UpdateSourceInfoAsync(); - + _logger.LogInformation("Fetch para {SourceName} completado. Se guardaron {Count} registros.", SourceName, cotizaciones.Count); return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros."); } - + private async Task UpdateSourceInfoAsync() { var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); diff --git a/src/Mercados.Infrastructure/DataFetchers/MercadoAgroFetcher.cs b/src/Mercados.Infrastructure/DataFetchers/MercadoAgroFetcher.cs index b6ad239..08af7b4 100644 --- a/src/Mercados.Infrastructure/DataFetchers/MercadoAgroFetcher.cs +++ b/src/Mercados.Infrastructure/DataFetchers/MercadoAgroFetcher.cs @@ -48,7 +48,7 @@ namespace Mercados.Infrastructure.DataFetchers await _cotizacionRepository.GuardarMuchosAsync(cotizaciones); await UpdateSourceInfoAsync(); - + _logger.LogInformation("Fetch para {SourceName} completado exitosamente. Se guardaron {Count} registros.", SourceName, cotizaciones.Count); return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros."); } @@ -61,7 +61,8 @@ namespace Mercados.Infrastructure.DataFetchers private async Task GetHtmlContentAsync() { - var client = _httpClientFactory.CreateClient(); + // Pedimos el cliente HTTP con el nombre específico que tiene la política de Polly + var client = _httpClientFactory.CreateClient("MercadoAgroFetcher"); // Es importante simular un navegador para evitar bloqueos. client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); @@ -142,7 +143,7 @@ namespace Mercados.Infrastructure.DataFetchers await _fuenteDatoRepository.ActualizarAsync(fuente); } } - + // --- Funciones de Ayuda para Parseo --- private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR"); private decimal ParseDecimal(string value) diff --git a/src/Mercados.Infrastructure/Mercados.Infrastructure.csproj b/src/Mercados.Infrastructure/Mercados.Infrastructure.csproj index 65da25c..07dfa60 100644 --- a/src/Mercados.Infrastructure/Mercados.Infrastructure.csproj +++ b/src/Mercados.Infrastructure/Mercados.Infrastructure.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Mercados.Worker/Program.cs b/src/Mercados.Worker/Program.cs index 745e9d8..36b7d16 100644 --- a/src/Mercados.Worker/Program.cs +++ b/src/Mercados.Worker/Program.cs @@ -4,6 +4,8 @@ using Mercados.Infrastructure.DataFetchers; using Mercados.Infrastructure.Persistence; using Mercados.Infrastructure.Persistence.Repositories; using Mercados.Worker; +using Polly; +using Polly.Extensions.Http; // Carga las variables de entorno desde el archivo .env en la raíz de la solución. DotNetEnv.Env.Load(); @@ -40,17 +42,56 @@ IHost host = Host.CreateDefaultBuilder(args) services.AddScoped(); services.AddScoped(); services.AddScoped(); - + // El cliente HTTP es fundamental para hacer llamadas a APIs externas. // Le damos un nombre al cliente de Finnhub para cumplir con los requisitos de su constructor. - services.AddHttpClient("Finnhub"); + //services.AddHttpClient("Finnhub"); + + // Configuramos CADA cliente HTTP que nuestros fetchers usan. + // IHttpClientFactory nos permite nombrar y configurar clientes de forma independiente. + + // Cliente para el scraper del MercadoAgro, con una política de reintentos + services.AddHttpClient("MercadoAgroFetcher") + .AddPolicyHandler(GetRetryPolicy()); + + // Cliente para la API de BCR, con la misma política de reintentos + services.AddHttpClient("BcrDataFetcher") + .AddPolicyHandler(GetRetryPolicy()); + + // Cliente para Finnhub, con la misma política de reintentos + services.AddHttpClient("FinnhubDataFetcher") + .AddPolicyHandler(GetRetryPolicy()); + + // Cliente para YahooFinance (aunque es menos probable que falle, es buena práctica incluirlo) + // La librería YahooFinanceApi usa su propio HttpClient, así que esta configuración + // no le afectará directamente. La resiliencia para YahooFinance la manejaremos de otra forma si es necesario. + // Por ahora, lo dejamos así y nos enfocamos en los que usan IHttpClientFactory. // --- 3. Registro del Worker Principal --- - + // Finalmente, registramos nuestro servicio de fondo (el worker en sí). services.AddHostedService(); }) .Build(); +// Esta función define nuestra política de reintentos. +static IAsyncPolicy GetRetryPolicy() +{ + // Polly.Extensions.Http nos da este método conveniente. + return HttpPolicyExtensions + // Maneja errores de red transitorios O códigos de estado de servidor que indican un problema temporal. + .HandleTransientHttpError() + // También maneja el error 408 Request Timeout + .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.RequestTimeout) + // Política de reintento con espera exponencial: 3 reintentos, esperando 2^intento segundos. + .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + onRetry: (outcome, timespan, retryAttempt, context) => + { + // Registramos un log cada vez que se realiza un reintento. + // Esta es una forma de hacerlo sin tener acceso directo al ILogger aquí. + Console.WriteLine($"[Polly] Reintentando petición... Intento {retryAttempt}. Esperando {timespan.TotalSeconds}s. Causa: {outcome.Exception?.Message ?? outcome.Result.ReasonPhrase}"); + }); +} + await host.RunAsync(); \ No newline at end of file