Retry Fix 504 Timeout

This commit is contained in:
2025-08-17 20:47:51 -03:00
parent eed8d2f065
commit a4e47b6e3d
2 changed files with 45 additions and 8 deletions

View File

@@ -38,14 +38,16 @@ builder.Services.AddHttpClient("ElectoralApiClient", client =>
client.BaseAddress = new Uri(baseUrl); client.BaseAddress = new Uri(baseUrl);
} }
// Limpiamos los headers por defecto y añadimos uno que simula ser un navegador. // --- TIMEOUT MÁS LARGO ---
// Esto es crucial para pasar a través de Firewalls de Aplicaciones Web (WAFs) // Aumentamos el tiempo de espera a 90 segundos.
// que bloquean clientes automatizados no reconocidos. // Esto le dará a las peticiones lentas de la API tiempo suficiente para responder.
client.Timeout = TimeSpan.FromSeconds(90);
client.DefaultRequestHeaders.Clear(); client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"); client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
client.DefaultRequestHeaders.Add("Accept", "*/*"); // Opcional, pero ayuda a parecerse más a Postman/navegador client.DefaultRequestHeaders.Add("Accept", "*/*");
client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br"); // Opcional client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
client.DefaultRequestHeaders.Add("Connection", "keep-alive"); // Opcional client.DefaultRequestHeaders.Add("Connection", "keep-alive");
}) })
.ConfigurePrimaryHttpMessageHandler(() => .ConfigurePrimaryHttpMessageHandler(() =>

View File

@@ -498,54 +498,84 @@ public class Worker : BackgroundService
} }
} }
/// <summary>
/// Obtiene y actualiza el estado general del recuento a nivel provincial para CADA categoría electoral.
/// Esta versión es robusta: consulta dinámicamente las categorías, usa la clave primaria compuesta
/// de la base de datos y guarda todos los cambios en una única transacción al final.
/// </summary>
/// <param name="authToken">El token de autenticación válido para la sesión.</param>
/// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
private async Task SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken) private async Task SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken)
{ {
try try
{ {
// PASO 1: Crear un "scope" para obtener una instancia fresca de DbContext.
// Esto es una práctica recomendada para servicios de larga duración para evitar problemas de concurrencia.
using var scope = _serviceProvider.CreateScope(); using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
// PASO 2: Obtener el ámbito geográfico de la Provincia.
// Necesitamos este objeto para obtener su 'DistritoId' ("02"), que es requerido por la API.
var provincia = await dbContext.AmbitosGeograficos var provincia = await dbContext.AmbitosGeograficos
.AsNoTracking() .AsNoTracking() // Optimización: Solo necesitamos leer datos, no modificarlos.
.FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);
// Comprobación de seguridad: Si la sincronización inicial falló y no tenemos el registro de la provincia,
// no podemos continuar. Registramos una advertencia y salimos del método.
if (provincia == null) if (provincia == null)
{ {
_logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) en la BD. Omitiendo sondeo de estado general."); _logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) en la BD. Omitiendo sondeo de estado general.");
return; return;
} }
// PASO 3: Obtener todas las categorías electorales disponibles desde nuestra base de datos.
// Esto hace que el método sea dinámico y no dependa de IDs fijos en el código.
var categoriasParaSondear = await dbContext.CategoriasElectorales var categoriasParaSondear = await dbContext.CategoriasElectorales
.AsNoTracking() .AsNoTracking()
.ToListAsync(stoppingToken); .ToListAsync(stoppingToken);
if (!categoriasParaSondear.Any()) if (!categoriasParaSondear.Any())
{ {
_logger.LogWarning("No hay categorías en la BD para sondear el estado general del recuento."); _logger.LogWarning("No hay categorías en la BD para sondear el estado general del recuento.");
return; return;
} }
_logger.LogInformation("Iniciando sondeo de Estado Recuento General para {count} categorías...", categoriasParaSondear.Count); _logger.LogInformation("Iniciando sondeo de Estado Recuento General para {count} categorías...", categoriasParaSondear.Count);
// PASO 4: Iterar sobre cada categoría para obtener su estado de recuento individual.
foreach (var categoria in categoriasParaSondear) foreach (var categoria in categoriasParaSondear)
{ {
// Salimos limpiamente del bucle si la aplicación se está deteniendo.
if (stoppingToken.IsCancellationRequested) break; if (stoppingToken.IsCancellationRequested) break;
// Llamamos a la API con el distrito y la CATEGORÍA ACTUAL del bucle.
var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id); var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id);
// Solo procedemos si la API devolvió datos válidos.
if (estadoDto != null) if (estadoDto != null)
{ {
// Lógica "Upsert" (Update or Insert):
// Buscamos un registro existente usando la CLAVE PRIMARIA COMPUESTA.
var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync( var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(
new object[] { provincia.Id, categoria.Id }, new object[] { provincia.Id, categoria.Id },
cancellationToken: stoppingToken cancellationToken: stoppingToken
); );
// Si no se encuentra (FindAsync devuelve null), es un registro nuevo.
if (registroDb == null) if (registroDb == null)
{ {
// Creamos una nueva instancia de la entidad.
registroDb = new EstadoRecuentoGeneral registroDb = new EstadoRecuentoGeneral
{ {
AmbitoGeograficoId = provincia.Id, AmbitoGeograficoId = provincia.Id,
CategoriaId = categoria.Id CategoriaId = categoria.Id // Asignamos ambas partes de la clave primaria.
}; };
// Y la añadimos al ChangeTracker de EF para que la inserte en la BD.
dbContext.EstadosRecuentosGenerales.Add(registroDb); dbContext.EstadosRecuentosGenerales.Add(registroDb);
} }
// Mapeamos los datos del DTO de la API a nuestra entidad de base de datos.
// Esto se hace tanto para registros nuevos como para los existentes que se van a actualizar.
registroDb.MesasEsperadas = estadoDto.MesasEsperadas; registroDb.MesasEsperadas = estadoDto.MesasEsperadas;
registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas; registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas;
registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje; registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje;
@@ -554,11 +584,16 @@ public class Worker : BackgroundService
registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje; registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje;
} }
} }
// PASO 5: Guardar todos los cambios en la base de datos.
// Al llamar a SaveChangesAsync UNA SOLA VEZ fuera del bucle, EF Core agrupa
// todas las inserciones y actualizaciones en una única transacción eficiente.
await dbContext.SaveChangesAsync(stoppingToken); await dbContext.SaveChangesAsync(stoppingToken);
_logger.LogInformation("Sondeo de Estado Recuento General completado para todas las categorías."); _logger.LogInformation("Sondeo de Estado Recuento General completado para todas las categorías.");
} }
catch (Exception ex) catch (Exception ex)
{ {
// Capturamos cualquier excepción inesperada para que no detenga el worker y la registramos.
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General."); _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General.");
} }
} }