Fix Goteo solo para Telegramas
This commit is contained in:
		| @@ -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,28 +16,29 @@ public class ElectoralApiService : IElectoralApiService | ||||
|     private readonly IHttpClientFactory _httpClientFactory; | ||||
|     private readonly IConfiguration _configuration; | ||||
|     private readonly ILogger<ElectoralApiService> _logger; | ||||
|     private readonly RateLimiter _rateLimiter; | ||||
|     //private readonly RateLimiter _rateLimiter; | ||||
|  | ||||
|     public ElectoralApiService(IHttpClientFactory httpClientFactory, | ||||
|         IConfiguration configuration, | ||||
|         ILogger<ElectoralApiService> logger, | ||||
|         RateLimiter rateLimiter) | ||||
|         ILogger<ElectoralApiService> logger) | ||||
|         //RateLimiter rateLimiter) | ||||
|     { | ||||
|         _httpClientFactory = httpClientFactory; | ||||
|         _configuration = configuration; | ||||
|         _logger = logger; | ||||
|         _rateLimiter = rateLimiter; | ||||
|         //_rateLimiter = rateLimiter; | ||||
|     } | ||||
|  | ||||
|     public async Task<TokenResponse?> 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); | ||||
|  | ||||
|                 // 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"]; | ||||
| @@ -52,60 +53,62 @@ public class ElectoralApiService : IElectoralApiService | ||||
|  | ||||
|         // Ahora esto es válido, porque el método devuelve Task<TokenResponse?> | ||||
|         return await response.Content.ReadFromJsonAsync<TokenResponse>(); | ||||
|         } | ||||
|  | ||||
|         /* } | ||||
|          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||
|         return null; | ||||
|          return null;*/ | ||||
|     } | ||||
|  | ||||
|     public async Task<CatalogoDto?> 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); | ||||
|  | ||||
|                 // 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<CatalogoDto>() : null; | ||||
|         } | ||||
|         /* } | ||||
|          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||
|         return null; | ||||
|          return null;*/ | ||||
|     } | ||||
|  | ||||
|     public async Task<List<AgrupacionDto>?> 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); | ||||
|  | ||||
|                 // 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<List<AgrupacionDto>>() : null; | ||||
|         } | ||||
|         /* } | ||||
|          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||
|         return null; | ||||
|          return null;*/ | ||||
|     } | ||||
|  | ||||
|     public async Task<ResultadosDto?> 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); | ||||
|  | ||||
|                 // Si se nos concede el permiso para proceder... | ||||
|                 if (lease.IsAcquired) | ||||
|         { | ||||
|                 {*/ | ||||
|         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||
|  | ||||
|         // Construimos la URL base | ||||
| @@ -121,20 +124,21 @@ public class ElectoralApiService : IElectoralApiService | ||||
|         request.Headers.Add("Authorization", $"Bearer {authToken}"); | ||||
|         var response = await client.SendAsync(request); | ||||
|         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<ResultadosDto>() : null; | ||||
|         } | ||||
|         /* } | ||||
|          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||
|         return null; | ||||
|          return null;*/ | ||||
|     } | ||||
|  | ||||
|     public async Task<RepartoBancasDto?> 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); | ||||
|  | ||||
|                 // 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}"; | ||||
|  | ||||
| @@ -181,20 +185,21 @@ public class ElectoralApiService : IElectoralApiService | ||||
|  | ||||
|         // 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; | ||||
|          return null;*/ | ||||
|     } | ||||
|  | ||||
|     public async Task<List<string>?> 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); | ||||
|  | ||||
|                 // 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}"; | ||||
|  | ||||
| @@ -209,20 +214,21 @@ public class ElectoralApiService : IElectoralApiService | ||||
|  | ||||
|         // Ahora deserializamos al tipo correcto: List<string> | ||||
|         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<string>>() : null; | ||||
|         } | ||||
|         /* } | ||||
|          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||
|         return null; | ||||
|          return null;*/ | ||||
|     } | ||||
|  | ||||
|     public async Task<TelegramaFileDto?> 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); | ||||
|  | ||||
|                 // 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); | ||||
| @@ -260,40 +266,42 @@ public class ElectoralApiService : IElectoralApiService | ||||
|         // 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; | ||||
|          return null;*/ | ||||
|     } | ||||
|  | ||||
|     public async Task<ResumenDto?> 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); | ||||
|  | ||||
|                 // 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<ResumenDto>() : null; | ||||
|         } | ||||
|         /* } | ||||
|          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||
|         return null; | ||||
|          return null;*/ | ||||
|     } | ||||
|  | ||||
|     public async Task<EstadoRecuentoGeneralDto?> 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); | ||||
|  | ||||
|                 // 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}"; | ||||
| @@ -301,27 +309,28 @@ public class ElectoralApiService : IElectoralApiService | ||||
|         request.Headers.Add("Authorization", $"Bearer {authToken}"); | ||||
|         var response = await client.SendAsync(request); | ||||
|         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<EstadoRecuentoGeneralDto>() : null; | ||||
|         } | ||||
|         /* } | ||||
|          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||
|         return null; | ||||
|          return null;*/ | ||||
|     } | ||||
|  | ||||
|     public async Task<List<CategoriaDto>?> 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); | ||||
|  | ||||
|                 // 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<List<CategoriaDto>>() : null; | ||||
|         } | ||||
|         /* } | ||||
|          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||
|         return null; | ||||
|          return null;*/ | ||||
|     } | ||||
| } | ||||
| @@ -83,7 +83,7 @@ builder.Services.AddHttpClient("ElectoralApiClient", client => | ||||
| .AddPolicyHandler(GetRetryPolicy()); | ||||
|  | ||||
| // --- LIMITADOR DE VELOCIDAD BASADO EN TOKEN BUCKET --- | ||||
| builder.Services.AddSingleton<RateLimiter>(sp =>  | ||||
| /*builder.Services.AddSingleton<RateLimiter>(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<RateLimiter>(sp => | ||||
|         // Cómo se comporta cuando la cola está llena. | ||||
|         QueueProcessingOrder = QueueProcessingOrder.OldestFirst | ||||
|     })); | ||||
|  | ||||
| */ | ||||
| builder.Services.AddScoped<IElectoralApiService, ElectoralApiService>();  | ||||
|  | ||||
| builder.Services.AddHostedService<Worker>(); | ||||
|   | ||||
| @@ -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<EleccionesDbContext>(); | ||||
|             _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<EleccionesDbContext>(); | ||||
|  | ||||
|             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<Telegrama>(); | ||||
|  | ||||
|             var tareas = combinaciones.Select(async item => | ||||
|                 foreach (var categoria in categorias) | ||||
|                 { | ||||
|                 await semaforo.WaitAsync(stoppingToken); | ||||
|                 try | ||||
|                 { | ||||
|                     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<EleccionesDbContext>(); | ||||
|  | ||||
|                         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()) | ||||
|                         { | ||||
|                             _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) | ||||
|                             { | ||||
|                                 if (stoppingToken.IsCancellationRequested) return; | ||||
|  | ||||
|                                 var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); | ||||
|                                 if (telegramaFile != null) | ||||
|                                 { | ||||
|                                     var nuevoTelegrama = new Telegrama | ||||
|                                     { | ||||
|                                         Id = telegramaFile.NombreArchivo, | ||||
|                                     AmbitoGeograficoId = item.partido.Id, | ||||
|                                         AmbitoGeograficoId = partido.Id, | ||||
|                                         ContenidoBase64 = telegramaFile.Imagen, | ||||
|                                         FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), | ||||
|                                         FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() | ||||
|                                     }; | ||||
|                                 telegramasNuevosParaGuardar.Add(nuevoTelegrama); | ||||
|                                     await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken); | ||||
|                                 } | ||||
|                             // Si telegramaFile es null (por 403, 200 vacío, etc.), simplemente no hacemos nada. | ||||
|                         })); | ||||
|                                 // PAUSA DELIBERADA: Esperamos un poco para no parecer un bot. | ||||
|                                 await Task.Delay(250, stoppingToken); // 250ms de espera = 4 peticiones/segundo máximo. | ||||
|                             } | ||||
|                             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<EleccionesDbContext>(); | ||||
|                 await finalDbContext.Telegramas.AddRangeAsync(telegramasNuevosParaGuardar, stoppingToken); | ||||
|                 await finalDbContext.SaveChangesAsync(stoppingToken); | ||||
|                     } | ||||
|  | ||||
|             _logger.LogInformation( | ||||
|                 "Sondeo de Telegramas completado. Se guardaron {count} nuevos telegramas.", telegramasNuevosParaGuardar.Count); | ||||
|                     // PAUSA DELIBERADA: Esperamos un poco entre cada consulta de lista de telegramas. | ||||
|                     await Task.Delay(100, stoppingToken); | ||||
|                 } | ||||
|             } | ||||
|             _logger.LogInformation("Sondeo de Telegramas completado."); | ||||
|         } | ||||
|         catch (OperationCanceledException) | ||||
|         { | ||||
|             _logger.LogInformation("Sondeo de telegramas cancelado."); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user