feat(Worker): Implementa política de reintentos con Polly para resiliencia en la obtención de datos
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
// ADRs Argentinos
|
// ADRs Argentinos
|
||||||
"YPF", "GGAL", "BMA", "LOMA", "PAM", "TEO", "TGS", "EDN", "CRESY", "CEPU", "BBAR"
|
"YPF", "GGAL", "BMA", "LOMA", "PAM", "TEO", "TGS", "EDN", "CRESY", "CEPU", "BBAR"
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly FinnhubClient _client;
|
private readonly FinnhubClient _client;
|
||||||
private readonly ICotizacionBolsaRepository _cotizacionRepository;
|
private readonly ICotizacionBolsaRepository _cotizacionRepository;
|
||||||
private readonly IFuenteDatoRepository _fuenteDatoRepository;
|
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)");
|
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;
|
||||||
@@ -46,7 +47,7 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
|
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
|
||||||
var cotizaciones = new List<CotizacionBolsa>();
|
var cotizaciones = new List<CotizacionBolsa>();
|
||||||
|
|
||||||
foreach (var ticker in _tickers)
|
foreach (var ticker in _tickers)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -56,7 +57,7 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
if (quote.Current == 0 || quote.PreviousClose == 0) continue;
|
if (quote.Current == 0 || quote.PreviousClose == 0) continue;
|
||||||
|
|
||||||
var pctChange = ((quote.Current - quote.PreviousClose) / quote.PreviousClose) * 100;
|
var pctChange = ((quote.Current - quote.PreviousClose) / quote.PreviousClose) * 100;
|
||||||
|
|
||||||
cotizaciones.Add(new CotizacionBolsa
|
cotizaciones.Add(new CotizacionBolsa
|
||||||
{
|
{
|
||||||
Ticker = ticker,
|
Ticker = ticker,
|
||||||
@@ -79,11 +80,11 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
|
|
||||||
await _cotizacionRepository.GuardarMuchosAsync(cotizaciones);
|
await _cotizacionRepository.GuardarMuchosAsync(cotizaciones);
|
||||||
await UpdateSourceInfoAsync();
|
await UpdateSourceInfoAsync();
|
||||||
|
|
||||||
_logger.LogInformation("Fetch para {SourceName} completado. Se guardaron {Count} registros.", SourceName, cotizaciones.Count);
|
_logger.LogInformation("Fetch para {SourceName} completado. Se guardaron {Count} registros.", SourceName, cotizaciones.Count);
|
||||||
return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros.");
|
return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateSourceInfoAsync()
|
private async Task UpdateSourceInfoAsync()
|
||||||
{
|
{
|
||||||
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
|
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
|
|
||||||
await _cotizacionRepository.GuardarMuchosAsync(cotizaciones);
|
await _cotizacionRepository.GuardarMuchosAsync(cotizaciones);
|
||||||
await UpdateSourceInfoAsync();
|
await UpdateSourceInfoAsync();
|
||||||
|
|
||||||
_logger.LogInformation("Fetch para {SourceName} completado exitosamente. Se guardaron {Count} registros.", SourceName, cotizaciones.Count);
|
_logger.LogInformation("Fetch para {SourceName} completado exitosamente. Se guardaron {Count} registros.", SourceName, cotizaciones.Count);
|
||||||
return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros.");
|
return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros.");
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
|
|
||||||
@@ -142,7 +143,7 @@ namespace Mercados.Infrastructure.DataFetchers
|
|||||||
await _fuenteDatoRepository.ActualizarAsync(fuente);
|
await _fuenteDatoRepository.ActualizarAsync(fuente);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Funciones de Ayuda para Parseo ---
|
// --- Funciones de Ayuda para Parseo ---
|
||||||
private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR");
|
private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR");
|
||||||
private decimal ParseDecimal(string value)
|
private decimal ParseDecimal(string value)
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -40,17 +42,56 @@ IHost host = Host.CreateDefaultBuilder(args)
|
|||||||
services.AddScoped<IDataFetcher, BcrDataFetcher>();
|
services.AddScoped<IDataFetcher, BcrDataFetcher>();
|
||||||
services.AddScoped<IDataFetcher, FinnhubDataFetcher>();
|
services.AddScoped<IDataFetcher, FinnhubDataFetcher>();
|
||||||
services.AddScoped<IDataFetcher, YahooFinanceDataFetcher>();
|
services.AddScoped<IDataFetcher, YahooFinanceDataFetcher>();
|
||||||
|
|
||||||
// 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 ---
|
||||||
|
|
||||||
// Finalmente, registramos nuestro servicio de fondo (el worker en sí).
|
// Finalmente, registramos nuestro servicio de fondo (el worker en sí).
|
||||||
services.AddHostedService<DataFetchingService>();
|
services.AddHostedService<DataFetchingService>();
|
||||||
})
|
})
|
||||||
.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();
|
||||||
Reference in New Issue
Block a user