From 19b37f73206d043982fc77f8c2359f2598889b64 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 20 Aug 2025 16:28:17 -0300 Subject: [PATCH] Fix Goteo solo para Telegramas --- .../Services/ElectoralApiService.cs | 437 +++++++++--------- .../src/Elecciones.Worker/Program.cs | 4 +- .../src/Elecciones.Worker/Worker.cs | 130 +++--- 3 files changed, 277 insertions(+), 294 deletions(-) diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs b/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs index 2ed7007..748aef8 100644 --- a/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs +++ b/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs @@ -7,7 +7,7 @@ using System.Text.Json; using Microsoft.Extensions.Logging; using System.Threading.Tasks; using static Elecciones.Core.DTOs.BancaDto; -using System.Threading.RateLimiting; +//using System.Threading.RateLimiting; namespace Elecciones.Infrastructure.Services; @@ -16,312 +16,321 @@ public class ElectoralApiService : IElectoralApiService private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly ILogger _logger; - private readonly RateLimiter _rateLimiter; + //private readonly RateLimiter _rateLimiter; public ElectoralApiService(IHttpClientFactory httpClientFactory, IConfiguration configuration, - ILogger logger, - RateLimiter rateLimiter) + ILogger logger) + //RateLimiter rateLimiter) { _httpClientFactory = httpClientFactory; _configuration = configuration; _logger = logger; - _rateLimiter = rateLimiter; + //_rateLimiter = rateLimiter; } public async Task GetAuthTokenAsync() { // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. - using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); + /* + using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); - // Si se nos concede el permiso para proceder... - if (lease.IsAcquired) - { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - var username = _configuration["ElectoralApi:Username"]; - var password = _configuration["ElectoralApi:Password"]; - if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) return null; + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) + {*/ + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + var username = _configuration["ElectoralApi:Username"]; + var password = _configuration["ElectoralApi:Password"]; + if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) return null; - var request = new HttpRequestMessage(HttpMethod.Get, "/api/createtoken"); - request.Headers.Add("username", username); - request.Headers.Add("password", password); + var request = new HttpRequestMessage(HttpMethod.Get, "/api/createtoken"); + request.Headers.Add("username", username); + request.Headers.Add("password", password); - var response = await client.SendAsync(request); - if (!response.IsSuccessStatusCode) return null; + var response = await client.SendAsync(request); + if (!response.IsSuccessStatusCode) return null; - // Ahora esto es válido, porque el método devuelve Task - return await response.Content.ReadFromJsonAsync(); - } - - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. - return null; + // Ahora esto es válido, porque el método devuelve Task + return await response.Content.ReadFromJsonAsync(); + /* } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null;*/ } public async Task GetCatalogoAmbitosAsync(string authToken, int categoriaId) { // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. - using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); + /* + using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); - // Si se nos concede el permiso para proceder... - if (lease.IsAcquired) - { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - var request = new HttpRequestMessage(HttpMethod.Get, $"/api/catalogo/getCatalogo?categoriaId={categoriaId}"); - request.Headers.Add("Authorization", $"Bearer {authToken}"); - var response = await client.SendAsync(request); - return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; - } - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. - return null; + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) + {*/ + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + var request = new HttpRequestMessage(HttpMethod.Get, $"/api/catalogo/getCatalogo?categoriaId={categoriaId}"); + request.Headers.Add("Authorization", $"Bearer {authToken}"); + var response = await client.SendAsync(request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; + /* } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null;*/ } public async Task?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId) { // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. - using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); + /* + using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); - // Si se nos concede el permiso para proceder... - if (lease.IsAcquired) - { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - var requestUri = $"/api/catalogo/getAgrupaciones?distritoId={distritoId}&categoriaId={categoriaId}"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Add("Authorization", $"Bearer {authToken}"); - var response = await client.SendAsync(request); - return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync>() : null; - } - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. - return null; + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) + {*/ + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + var requestUri = $"/api/catalogo/getAgrupaciones?distritoId={distritoId}&categoriaId={categoriaId}"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", $"Bearer {authToken}"); + var response = await client.SendAsync(request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync>() : null; + /* } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null;*/ } public async Task GetResultadosAsync(string authToken, string distritoId, string seccionId, string? municipioId, int categoriaId) { // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. - using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); + /* + using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); - // Si se nos concede el permiso para proceder... - if (lease.IsAcquired) + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) + {*/ + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + + // Construimos la URL base + var requestUri = $"/api/resultados/getResultados?distritoId={distritoId}&seccionId={seccionId}&categoriaId={categoriaId}"; + + // Añadimos el municipioId a la URL SÓLO si no es nulo o vacío + if (!string.IsNullOrEmpty(municipioId)) { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - - // Construimos la URL base - var requestUri = $"/api/resultados/getResultados?distritoId={distritoId}&seccionId={seccionId}&categoriaId={categoriaId}"; - - // Añadimos el municipioId a la URL SÓLO si no es nulo o vacío - if (!string.IsNullOrEmpty(municipioId)) - { - requestUri += $"&municipioId={municipioId}"; - } - - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Add("Authorization", $"Bearer {authToken}"); - var response = await client.SendAsync(request); - return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; + requestUri += $"&municipioId={municipioId}"; } - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. - return null; + + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", $"Bearer {authToken}"); + var response = await client.SendAsync(request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; + /* } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null;*/ } public async Task GetBancasAsync(string authToken, string distritoId, string? seccionProvincialId, int categoriaId) { // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. - using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); + /* + using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); - // Si se nos concede el permiso para proceder... - if (lease.IsAcquired) + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) + {*/ + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={categoriaId}"; + + if (!string.IsNullOrEmpty(seccionProvincialId)) { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={categoriaId}"; + requestUri += $"&seccionProvincialId={seccionProvincialId}"; + } - if (!string.IsNullOrEmpty(seccionProvincialId)) - { - requestUri += $"&seccionProvincialId={seccionProvincialId}"; - } + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", $"Bearer {authToken}"); - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Add("Authorization", $"Bearer {authToken}"); - - HttpResponseMessage response; - try - { - response = await client.SendAsync(request); - } - catch (Exception ex) - { - // Captura errores de red (ej. la API se cae momentáneamente) - _logger.LogError(ex, "La petición HTTP a getBancas falló. URI: {requestUri}", requestUri); - return null; - } - - // Comprobamos que la respuesta fue exitosa Y que contiene datos antes de intentar leerla. - if (response.IsSuccessStatusCode && response.Content?.Headers.ContentLength > 0) - { - try - { - // Solo si hay contenido, intentamos deserializar. - return await response.Content.ReadFromJsonAsync(); - } - catch (JsonException ex) - { - // Si el contenido no es un JSON válido, lo registramos y devolvemos null. - _logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para getBancas. URI: {requestUri}, Status: {statusCode}", requestUri, response.StatusCode); - return null; - } - } - else if (!response.IsSuccessStatusCode) - { - // Si la API devolvió un error HTTP, lo registramos. - _logger.LogWarning("La API devolvió un código de error {statusCode} para getBancas. URI: {requestUri}", response.StatusCode, requestUri); - } - - // Si la respuesta fue 200 OK pero con cuerpo vacío, o si fue un error HTTP, devolvemos null. + HttpResponseMessage response; + try + { + response = await client.SendAsync(request); + } + catch (Exception ex) + { + // Captura errores de red (ej. la API se cae momentáneamente) + _logger.LogError(ex, "La petición HTTP a getBancas falló. URI: {requestUri}", requestUri); return null; } - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + + // Comprobamos que la respuesta fue exitosa Y que contiene datos antes de intentar leerla. + if (response.IsSuccessStatusCode && response.Content?.Headers.ContentLength > 0) + { + try + { + // Solo si hay contenido, intentamos deserializar. + return await response.Content.ReadFromJsonAsync(); + } + catch (JsonException ex) + { + // Si el contenido no es un JSON válido, lo registramos y devolvemos null. + _logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para getBancas. URI: {requestUri}, Status: {statusCode}", requestUri, response.StatusCode); + return null; + } + } + else if (!response.IsSuccessStatusCode) + { + // Si la API devolvió un error HTTP, lo registramos. + _logger.LogWarning("La API devolvió un código de error {statusCode} para getBancas. URI: {requestUri}", response.StatusCode, requestUri); + } + + // Si la respuesta fue 200 OK pero con cuerpo vacío, o si fue un error HTTP, devolvemos null. return null; + /* } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null;*/ } public async Task?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = null) { // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. - using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); + /* + using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); - // Si se nos concede el permiso para proceder... - if (lease.IsAcquired) + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) + {*/ + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + var requestUri = $"/api/resultados/getTelegramasTotalizados?distritoId={distritoId}&seccionId={seccionId}"; + + if (categoriaId.HasValue) { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - var requestUri = $"/api/resultados/getTelegramasTotalizados?distritoId={distritoId}&seccionId={seccionId}"; - - if (categoriaId.HasValue) - { - requestUri += $"&categoriaId={categoriaId.Value}"; - } - - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Add("Authorization", $"Bearer {authToken}"); - var response = await client.SendAsync(request); - - // Ahora deserializamos al tipo correcto: List - return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync>() : null; + requestUri += $"&categoriaId={categoriaId.Value}"; } - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. - return null; + + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", $"Bearer {authToken}"); + var response = await client.SendAsync(request); + + // Ahora deserializamos al tipo correcto: List + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync>() : null; + /* } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null;*/ } public async Task GetTelegramaFileAsync(string authToken, string mesaId) { // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. - using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); + /* + using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); - // Si se nos concede el permiso para proceder... - if (lease.IsAcquired) + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) + {*/ + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + var requestUri = $"/api/resultados/getFile?mesaId={mesaId}"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", $"Bearer {authToken}"); + + HttpResponseMessage response; + try { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - var requestUri = $"/api/resultados/getFile?mesaId={mesaId}"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Add("Authorization", $"Bearer {authToken}"); - - HttpResponseMessage response; - try - { - response = await client.SendAsync(request); - } - catch (Exception ex) - { - _logger.LogError(ex, "La petición HTTP a getFile falló para la mesa {mesaId}", mesaId); - return null; - } - - if (response.IsSuccessStatusCode && response.Content?.Headers.ContentLength > 0) - { - try - { - return await response.Content.ReadFromJsonAsync(); - } - catch (JsonException ex) - { - // Si la deserialización falla, ahora lo sabremos exactamente. - _logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para la mesa {mesaId}. Status: {statusCode}", mesaId, response.StatusCode); - return null; - } - } - else if (!response.IsSuccessStatusCode) - { - _logger.LogWarning("La API devolvió un código de error {statusCode} para la mesa {mesaId}", response.StatusCode, mesaId); - } - // Si la respuesta fue exitosa pero sin contenido, no es necesario loguearlo como un error, - // simplemente devolvemos null y el Worker lo ignorará. - + response = await client.SendAsync(request); + } + catch (Exception ex) + { + _logger.LogError(ex, "La petición HTTP a getFile falló para la mesa {mesaId}", mesaId); return null; } - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + + if (response.IsSuccessStatusCode && response.Content?.Headers.ContentLength > 0) + { + try + { + return await response.Content.ReadFromJsonAsync(); + } + catch (JsonException ex) + { + // Si la deserialización falla, ahora lo sabremos exactamente. + _logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para la mesa {mesaId}. Status: {statusCode}", mesaId, response.StatusCode); + return null; + } + } + else if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("La API devolvió un código de error {statusCode} para la mesa {mesaId}", response.StatusCode, mesaId); + } + // Si la respuesta fue exitosa pero sin contenido, no es necesario loguearlo como un error, + // simplemente devolvemos null y el Worker lo ignorará. + return null; + /* } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null;*/ } public async Task GetResumenAsync(string authToken, string distritoId) { // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. - using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); + /* + using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); - // Si se nos concede el permiso para proceder... - if (lease.IsAcquired) - { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - var requestUri = $"/api/resultados/getResumen?distritoId={distritoId}"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Add("Authorization", $"Bearer {authToken}"); - var response = await client.SendAsync(request); - return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; - } - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. - return null; + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) + {*/ + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + var requestUri = $"/api/resultados/getResumen?distritoId={distritoId}"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", $"Bearer {authToken}"); + var response = await client.SendAsync(request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; + /* } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null;*/ } public async Task GetEstadoRecuentoGeneralAsync(string authToken, string distritoId, int categoriaId) { // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. - using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); + /* + using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); - // Si se nos concede el permiso para proceder... - if (lease.IsAcquired) - { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - // La URL ahora usa el parámetro 'categoriaId' que se recibe - var requestUri = $"/api/estados/estadoRecuento?distritoId={distritoId}&categoriaId={categoriaId}"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Add("Authorization", $"Bearer {authToken}"); - var response = await client.SendAsync(request); - return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; - } - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. - return null; + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) + {*/ + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + // La URL ahora usa el parámetro 'categoriaId' que se recibe + var requestUri = $"/api/estados/estadoRecuento?distritoId={distritoId}&categoriaId={categoriaId}"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", $"Bearer {authToken}"); + var response = await client.SendAsync(request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; + /* } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null;*/ } public async Task?> GetCategoriasAsync(string authToken) { // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. - using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); + /* + using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); - // Si se nos concede el permiso para proceder... - if (lease.IsAcquired) - { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - var request = new HttpRequestMessage(HttpMethod.Get, "/api/catalogo/getCategorias"); - request.Headers.Add("Authorization", $"Bearer {authToken}"); - var response = await client.SendAsync(request); - return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync>() : null; - } - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. - return null; + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) + {*/ + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + var request = new HttpRequestMessage(HttpMethod.Get, "/api/catalogo/getCategorias"); + request.Headers.Add("Authorization", $"Bearer {authToken}"); + var response = await client.SendAsync(request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync>() : null; + /* } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null;*/ } } \ No newline at end of file diff --git a/Elecciones-Web/src/Elecciones.Worker/Program.cs b/Elecciones-Web/src/Elecciones.Worker/Program.cs index 945cf46..47c17a4 100644 --- a/Elecciones-Web/src/Elecciones.Worker/Program.cs +++ b/Elecciones-Web/src/Elecciones.Worker/Program.cs @@ -83,7 +83,7 @@ builder.Services.AddHttpClient("ElectoralApiClient", client => .AddPolicyHandler(GetRetryPolicy()); // --- LIMITADOR DE VELOCIDAD BASADO EN TOKEN BUCKET --- -builder.Services.AddSingleton(sp => +/*builder.Services.AddSingleton(sp => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions { // El tamaño máximo del cubo (la ráfaga máxima que permitimos). @@ -101,7 +101,7 @@ builder.Services.AddSingleton(sp => // Cómo se comporta cuando la cola está llena. QueueProcessingOrder = QueueProcessingOrder.OldestFirst })); - +*/ builder.Services.AddScoped(); builder.Services.AddHostedService(); diff --git a/Elecciones-Web/src/Elecciones.Worker/Worker.cs b/Elecciones-Web/src/Elecciones.Worker/Worker.cs index 1dcdb5b..c2f54ac 100644 --- a/Elecciones-Web/src/Elecciones.Worker/Worker.cs +++ b/Elecciones-Web/src/Elecciones.Worker/Worker.cs @@ -603,111 +603,85 @@ public class Worker : BackgroundService { try { - // PASO 1: Obtener los datos base para las consultas. - // Usamos un DbContext inicial solo para leer los catálogos. - using var initialScope = _serviceProvider.CreateScope(); - var initialDbContext = initialScope.ServiceProvider.GetRequiredService(); + _logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas (modo de bajo perfil) ---"); - var partidos = await initialDbContext.AmbitosGeograficos + using var scope = _serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var partidos = await dbContext.AmbitosGeograficos .AsNoTracking() .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) .ToListAsync(stoppingToken); - var categorias = await initialDbContext.CategoriasElectorales + var categorias = await dbContext.CategoriasElectorales .AsNoTracking() .ToListAsync(stoppingToken); - if (!partidos.Any() || !categorias.Any()) + if (!partidos.Any() || !categorias.Any()) return; + + // --- LÓGICA DE GOTEO LENTO --- + // Procesamos una combinación (partido/categoría) a la vez. + foreach (var partido in partidos) { - _logger.LogWarning("No se encontraron partidos o categorías en la BD para sondear telegramas."); - return; - } - - // Creamos una lista de todas las consultas que necesitamos hacer (135 partidos * 3 categorías = 405 consultas). - var combinaciones = partidos.SelectMany(partido => categorias, (partido, categoria) => new { partido, categoria }); - - const int GRADO_DE_PARALELISMO = 3; - var semaforo = new SemaphoreSlim(GRADO_DE_PARALELISMO); - - _logger.LogInformation("Iniciando sondeo de Telegramas para {count} combinaciones... con paralelismo de {degree}", combinaciones.Count(), GRADO_DE_PARALELISMO); - - // Usaremos un ConcurrentBag para recolectar de forma segura los telegramas nuevos desde múltiples hilos. - var telegramasNuevosParaGuardar = new ConcurrentBag(); - - var tareas = combinaciones.Select(async item => - { - await semaforo.WaitAsync(stoppingToken); - try + foreach (var categoria in categorias) { - var idsDeApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, item.partido.DistritoId!, item.partido.SeccionId!, item.categoria.Id); + // Si la aplicación se apaga, salimos inmediatamente. + if (stoppingToken.IsCancellationRequested) return; - if (idsDeApi is { Count: > 0 }) + // Obtenemos la lista de IDs. + var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id); + + if (listaTelegramasApi is { Count: > 0 }) { + // Usamos un DbContext propio para este bloque para asegurar que los cambios se guarden. using var innerScope = _serviceProvider.CreateScope(); var innerDbContext = innerScope.ServiceProvider.GetRequiredService(); var idsYaEnDb = await innerDbContext.Telegramas - .Where(t => idsDeApi.Contains(t.Id)) + .Where(t => listaTelegramasApi.Contains(t.Id)) .Select(t => t.Id) .ToListAsync(stoppingToken); - var nuevosTelegramasIds = idsDeApi.Except(idsYaEnDb).ToList(); + var nuevosTelegramasIds = listaTelegramasApi.Except(idsYaEnDb).ToList(); - if (!nuevosTelegramasIds.Any()) return; - - _logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando en paralelo...", nuevosTelegramasIds.Count, item.partido.Nombre, item.categoria.Nombre); - - // --- NUEVA OPTIMIZACIÓN: Paralelizar la descarga de los archivos --- - await Task.WhenAll(nuevosTelegramasIds.Select(async mesaId => + if (nuevosTelegramasIds.Any()) { - var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); - if (telegramaFile != null) + _logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando...", nuevosTelegramasIds.Count, partido.Nombre, categoria.Nombre); + + // Descargamos los archivos de uno en uno, con una pausa entre cada uno. + foreach (var mesaId in nuevosTelegramasIds) { - var nuevoTelegrama = new Telegrama + if (stoppingToken.IsCancellationRequested) return; + + var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); + if (telegramaFile != null) { - Id = telegramaFile.NombreArchivo, - AmbitoGeograficoId = item.partido.Id, - ContenidoBase64 = telegramaFile.Imagen, - FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), - FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() - }; - telegramasNuevosParaGuardar.Add(nuevoTelegrama); + var nuevoTelegrama = new Telegrama + { + Id = telegramaFile.NombreArchivo, + AmbitoGeograficoId = partido.Id, + ContenidoBase64 = telegramaFile.Imagen, + FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), + FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() + }; + await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken); + } + // PAUSA DELIBERADA: Esperamos un poco para no parecer un bot. + await Task.Delay(250, stoppingToken); // 250ms de espera = 4 peticiones/segundo máximo. } - // Si telegramaFile es null (por 403, 200 vacío, etc.), simplemente no hacemos nada. - })); + await innerDbContext.SaveChangesAsync(stoppingToken); + } } - } - // --- NUEVA ROBUSTEZ: Capturar errores por tarea --- - catch (Exception ex) - { - // Si una combinación entera falla (ej. getTelegramasTotalizados da 500), lo logueamos - // pero NO relanzamos la excepción, para no cancelar el Task.WhenAll principal. - _logger.LogError(ex, "Falló el sondeo de telegramas para el partido '{partido}' y categoría '{cat}'", item.partido.Nombre, item.categoria.Nombre); - } - finally - { - semaforo.Release(); - // Añadir un pequeño retraso aleatorio - await Task.Delay(TimeSpan.FromMilliseconds(new Random().Next(50, 251)), stoppingToken); - } - }); - // Esperamos a que todas las tareas de sondeo y descarga terminen. - await Task.WhenAll(tareas); - - // --- Guardado Masivo Final --- - // Después de que todo el paralelismo ha terminado, hacemos una única operación de escritura en la BD. - if (!telegramasNuevosParaGuardar.IsEmpty) - { - _logger.LogInformation("Guardando un total de {count} telegramas nuevos en la base de datos...", telegramasNuevosParaGuardar.Count); - using var finalScope = _serviceProvider.CreateScope(); - var finalDbContext = finalScope.ServiceProvider.GetRequiredService(); - await finalDbContext.Telegramas.AddRangeAsync(telegramasNuevosParaGuardar, stoppingToken); - await finalDbContext.SaveChangesAsync(stoppingToken); + // PAUSA DELIBERADA: Esperamos un poco entre cada consulta de lista de telegramas. + await Task.Delay(100, stoppingToken); + } } - - _logger.LogInformation( - "Sondeo de Telegramas completado. Se guardaron {count} nuevos telegramas.", telegramasNuevosParaGuardar.Count); + _logger.LogInformation("Sondeo de Telegramas completado."); + } + catch (OperationCanceledException) + { + _logger.LogInformation("Sondeo de telegramas cancelado."); } catch (Exception ex) {