From a4e47b6e3d1f8b0746f4f910f56a94e17b2e030c Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sun, 17 Aug 2025 20:47:51 -0300 Subject: [PATCH] Retry Fix 504 Timeout --- .../src/Elecciones.Worker/Program.cs | 14 ++++--- .../src/Elecciones.Worker/Worker.cs | 39 ++++++++++++++++++- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/Elecciones-Web/src/Elecciones.Worker/Program.cs b/Elecciones-Web/src/Elecciones.Worker/Program.cs index eb9ff1e..74111b5 100644 --- a/Elecciones-Web/src/Elecciones.Worker/Program.cs +++ b/Elecciones-Web/src/Elecciones.Worker/Program.cs @@ -37,15 +37,17 @@ builder.Services.AddHttpClient("ElectoralApiClient", client => { client.BaseAddress = new Uri(baseUrl); } + + // --- TIMEOUT MÁS LARGO --- + // Aumentamos el tiempo de espera a 90 segundos. + // Esto le dará a las peticiones lentas de la API tiempo suficiente para responder. + client.Timeout = TimeSpan.FromSeconds(90); - // Limpiamos los headers por defecto y añadimos uno que simula ser un navegador. - // Esto es crucial para pasar a través de Firewalls de Aplicaciones Web (WAFs) - // que bloquean clientes automatizados no reconocidos. 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("Accept", "*/*"); // Opcional, pero ayuda a parecerse más a Postman/navegador - client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br"); // Opcional - client.DefaultRequestHeaders.Add("Connection", "keep-alive"); // Opcional + client.DefaultRequestHeaders.Add("Accept", "*/*"); + client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br"); + client.DefaultRequestHeaders.Add("Connection", "keep-alive"); }) .ConfigurePrimaryHttpMessageHandler(() => diff --git a/Elecciones-Web/src/Elecciones.Worker/Worker.cs b/Elecciones-Web/src/Elecciones.Worker/Worker.cs index be5a5b6..00267fc 100644 --- a/Elecciones-Web/src/Elecciones.Worker/Worker.cs +++ b/Elecciones-Web/src/Elecciones.Worker/Worker.cs @@ -498,54 +498,84 @@ public class Worker : BackgroundService } } + /// + /// 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. + /// + /// El token de autenticación válido para la sesión. + /// El token de cancelación para detener la operación. private async Task SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken) { 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(); var dbContext = scope.ServiceProvider.GetRequiredService(); + // 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 - .AsNoTracking() + .AsNoTracking() // Optimización: Solo necesitamos leer datos, no modificarlos. .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) { _logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) en la BD. Omitiendo sondeo de estado general."); 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 .AsNoTracking() .ToListAsync(stoppingToken); + if (!categoriasParaSondear.Any()) { _logger.LogWarning("No hay categorías en la BD para sondear el estado general del recuento."); return; } + _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) { + // Salimos limpiamente del bucle si la aplicación se está deteniendo. 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); + + // Solo procedemos si la API devolvió datos válidos. if (estadoDto != null) { + // Lógica "Upsert" (Update or Insert): + // Buscamos un registro existente usando la CLAVE PRIMARIA COMPUESTA. var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync( new object[] { provincia.Id, categoria.Id }, cancellationToken: stoppingToken ); + // Si no se encuentra (FindAsync devuelve null), es un registro nuevo. if (registroDb == null) { + // Creamos una nueva instancia de la entidad. registroDb = new EstadoRecuentoGeneral { 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); } + // 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.MesasTotalizadas = estadoDto.MesasTotalizadas; registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje; @@ -554,11 +584,16 @@ public class Worker : BackgroundService 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); _logger.LogInformation("Sondeo de Estado Recuento General completado para todas las categorías."); } 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."); } }