feat(Worker): Implementa política de reintentos con Polly para resiliencia en la obtención de datos

This commit is contained in:
2025-07-03 12:11:08 -03:00
parent 1730c66d6a
commit 6479a5a040
5 changed files with 58 additions and 13 deletions

View File

@@ -69,7 +69,8 @@ namespace Mercados.Infrastructure.DataFetchers
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
try 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); var token = await GetAuthTokenAsync(client);
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))

View File

@@ -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)"); 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; _cotizacionRepository = cotizacionRepository;
_fuenteDatoRepository = fuenteDatoRepository; _fuenteDatoRepository = fuenteDatoRepository;
_logger = logger; _logger = logger;

View File

@@ -61,7 +61,8 @@ namespace Mercados.Infrastructure.DataFetchers
private async Task<string> GetHtmlContentAsync() private async Task<string> 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. // 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"); 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");

View File

@@ -9,6 +9,7 @@
<PackageReference Include="Dapper" Version="2.1.66" /> <PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.6" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageReference Include="ThreeFourteen.Finnhub.Client" Version="1.2.0" /> <PackageReference Include="ThreeFourteen.Finnhub.Client" Version="1.2.0" />
<PackageReference Include="YahooFinanceApi" Version="2.3.3" /> <PackageReference Include="YahooFinanceApi" Version="2.3.3" />

View File

@@ -4,6 +4,8 @@ using Mercados.Infrastructure.DataFetchers;
using Mercados.Infrastructure.Persistence; using Mercados.Infrastructure.Persistence;
using Mercados.Infrastructure.Persistence.Repositories; using Mercados.Infrastructure.Persistence.Repositories;
using Mercados.Worker; 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. // Carga las variables de entorno desde el archivo .env en la raíz de la solución.
DotNetEnv.Env.Load(); DotNetEnv.Env.Load();
@@ -43,7 +45,27 @@ IHost host = Host.CreateDefaultBuilder(args)
// El cliente HTTP es fundamental para hacer llamadas a APIs externas. // 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. // 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 --- // --- 3. Registro del Worker Principal ---
@@ -53,4 +75,23 @@ IHost host = Host.CreateDefaultBuilder(args)
}) })
.Build(); .Build();
// Esta función define nuestra política de reintentos.
static IAsyncPolicy<HttpResponseMessage> 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(); await host.RunAsync();