Retry Fix 504 Timeout
This commit is contained in:
		| @@ -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(() => | ||||||
|   | |||||||
| @@ -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."); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user