diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs index 4aecd07..0d1528f 100644 --- a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs @@ -14,7 +14,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+68dce9415e165633856e4fae9b2d71cc07b4e2ff")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Elecciones.Api.csproj.nuget.dgspec.json b/Elecciones-Web/src/Elecciones.Api/obj/Elecciones.Api.csproj.nuget.dgspec.json index 0c35a9c..113b095 100644 --- a/Elecciones-Web/src/Elecciones.Api/obj/Elecciones.Api.csproj.nuget.dgspec.json +++ b/Elecciones-Web/src/Elecciones.Api/obj/Elecciones.Api.csproj.nuget.dgspec.json @@ -306,6 +306,10 @@ "Microsoft.Extensions.Http": { "target": "Package", "version": "[9.0.8, )" + }, + "System.Threading.RateLimiting": { + "target": "Package", + "version": "[9.0.8, )" } }, "imports": [ diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/Elecciones.Infrastructure.csproj b/Elecciones-Web/src/Elecciones.Infrastructure/Elecciones.Infrastructure.csproj index c53bc04..83ba870 100644 --- a/Elecciones-Web/src/Elecciones.Infrastructure/Elecciones.Infrastructure.csproj +++ b/Elecciones-Web/src/Elecciones.Infrastructure/Elecciones.Infrastructure.csproj @@ -7,6 +7,7 @@ + diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs b/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs index db39b88..bc2ba89 100644 --- a/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs +++ b/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs @@ -7,6 +7,7 @@ using System.Text.Json; using Microsoft.Extensions.Logging; using System.Threading.Tasks; using static Elecciones.Core.DTOs.BancaDto; +using System.Threading.RateLimiting; namespace Elecciones.Infrastructure.Services; @@ -15,205 +16,308 @@ public class ElectoralApiService : IElectoralApiService private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly ILogger _logger; + private readonly RateLimiter _rateLimiter; public ElectoralApiService(IHttpClientFactory httpClientFactory, - IConfiguration configuration, - ILogger logger) + IConfiguration configuration, + ILogger logger, + RateLimiter rateLimiter) { _httpClientFactory = httpClientFactory; _configuration = configuration; _logger = logger; + _rateLimiter = rateLimiter; } public async Task GetAuthTokenAsync() { - 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 response = await client.SendAsync(request); - if (!response.IsSuccessStatusCode) return null; - var tokenResponse = await response.Content.ReadFromJsonAsync(); - return (tokenResponse is { Success: true, Data.AccessToken: not null }) ? tokenResponse.Data.AccessToken : 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 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 response = await client.SendAsync(request); + if (!response.IsSuccessStatusCode) return null; + var tokenResponse = await response.Content.ReadFromJsonAsync(); + return (tokenResponse is { Success: true, Data.AccessToken: not null }) ? tokenResponse.Data.AccessToken : null; + } + // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. + return null; } public async Task GetCatalogoAmbitosAsync(string authToken, int categoriaId) { - 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; + // "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() : 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) { - 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; + // "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>() : 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) { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + // "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); - // 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)) + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) { - requestUri += $"&municipioId={municipioId}"; - } + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - 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; + // 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; + } + // 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) { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={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); - if (!string.IsNullOrEmpty(seccionProvincialId)) + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) { - requestUri += $"&seccionProvincialId={seccionProvincialId}"; - } + var client = _httpClientFactory.CreateClient("ElectoralApiClient"); + var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={categoriaId}"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Add("Authorization", $"Bearer {authToken}"); + if (!string.IsNullOrEmpty(seccionProvincialId)) + { + requestUri += $"&seccionProvincialId={seccionProvincialId}"; + } - 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; - } + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", $"Bearer {authToken}"); - // Comprobamos que la respuesta fue exitosa Y que contiene datos antes de intentar leerla. - if (response.IsSuccessStatusCode && response.Content?.Headers.ContentLength > 0) - { + HttpResponseMessage response; try { - // Solo si hay contenido, intentamos deserializar. - return await response.Content.ReadFromJsonAsync(); + response = await client.SendAsync(request); } - catch (JsonException ex) + catch (Exception 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); + // 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; } - } - 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. + // 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) { - var client = _httpClientFactory.CreateClient("ElectoralApiClient"); - var requestUri = $"/api/resultados/getTelegramasTotalizados?distritoId={distritoId}&seccionId={seccionId}"; + // "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); - if (categoriaId.HasValue) + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) { - requestUri += $"&categoriaId={categoriaId.Value}"; + 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; } - - 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) { - 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}"); + // "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); - HttpResponseMessage response; - try + // Si se nos concede el permiso para proceder... + if (lease.IsAcquired) { - 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; - } + 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}"); - if (response.IsSuccessStatusCode && response.Content?.Headers.ContentLength > 0) - { + HttpResponseMessage response; try { - return await response.Content.ReadFromJsonAsync(); + response = await client.SendAsync(request); } - catch (JsonException ex) + catch (Exception 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); + _logger.LogError(ex, "La petición HTTP a getFile falló para la mesa {mesaId}", mesaId); 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á. + 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) { - 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; + // "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() : 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) { - 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; + // "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}"; + 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) { - 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; + // "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>() : 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.Infrastructure/Services/RateLimiterService.cs b/Elecciones-Web/src/Elecciones.Infrastructure/Services/RateLimiterService.cs new file mode 100644 index 0000000..5818320 --- /dev/null +++ b/Elecciones-Web/src/Elecciones.Infrastructure/Services/RateLimiterService.cs @@ -0,0 +1,44 @@ +// Archivo: Elecciones.Infrastructure/Services/RateLimiterService.cs +using System.Threading; +using System.Threading.Tasks; + +namespace Elecciones.Infrastructure.Services; + +public class RateLimiterService +{ + private readonly SemaphoreSlim _semaphore; + private readonly int _limit; + private readonly TimeSpan _period; + private Timer _timer; + + public RateLimiterService(int limit, TimeSpan period) + { + _limit = limit; + _period = period; + _semaphore = new SemaphoreSlim(limit, limit); + + // Un temporizador que "rellena el cubo" cada 5 minutos. + _timer = new Timer( + callback: _ => RefillBucket(), + state: null, + dueTime: period, + period: period); + } + + private void RefillBucket() + { + // Calcula cuántas fichas faltan en el cubo. + var releaseCount = _limit - _semaphore.CurrentCount; + if (releaseCount > 0) + { + // Añade las fichas que faltan. + _semaphore.Release(releaseCount); + } + } + + // El método que usarán nuestras tareas para "pedir una ficha". + public async Task WaitAsync(CancellationToken cancellationToken = default) + { + await _semaphore.WaitAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs index 08fb351..7c238c4 100644 --- a/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+68dce9415e165633856e4fae9b2d71cc07b4e2ff")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/obj/Elecciones.Infrastructure.csproj.nuget.dgspec.json b/Elecciones-Web/src/Elecciones.Infrastructure/obj/Elecciones.Infrastructure.csproj.nuget.dgspec.json index 45d96a3..3924bca 100644 --- a/Elecciones-Web/src/Elecciones.Infrastructure/obj/Elecciones.Infrastructure.csproj.nuget.dgspec.json +++ b/Elecciones-Web/src/Elecciones.Infrastructure/obj/Elecciones.Infrastructure.csproj.nuget.dgspec.json @@ -126,6 +126,10 @@ "Microsoft.Extensions.Http": { "target": "Package", "version": "[9.0.8, )" + }, + "System.Threading.RateLimiting": { + "target": "Package", + "version": "[9.0.8, )" } }, "imports": [ diff --git a/Elecciones-Web/src/Elecciones.Worker/Elecciones.Worker.csproj b/Elecciones-Web/src/Elecciones.Worker/Elecciones.Worker.csproj index 151dee8..3d0b788 100644 --- a/Elecciones-Web/src/Elecciones.Worker/Elecciones.Worker.csproj +++ b/Elecciones-Web/src/Elecciones.Worker/Elecciones.Worker.csproj @@ -16,6 +16,7 @@ + diff --git a/Elecciones-Web/src/Elecciones.Worker/Program.cs b/Elecciones-Web/src/Elecciones.Worker/Program.cs index c77e39c..945cf46 100644 --- a/Elecciones-Web/src/Elecciones.Worker/Program.cs +++ b/Elecciones-Web/src/Elecciones.Worker/Program.cs @@ -11,6 +11,7 @@ using System.Net.Security; using System.Security.Authentication; using Polly; using Polly.Extensions.Http; +using System.Threading.RateLimiting; Log.Logger = new LoggerConfiguration() .WriteTo.Console() @@ -81,6 +82,26 @@ builder.Services.AddHttpClient("ElectoralApiClient", client => .AddPolicyHandler(GetRetryPolicy()); +// --- LIMITADOR DE VELOCIDAD BASADO EN TOKEN BUCKET --- +builder.Services.AddSingleton(sp => + new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions + { + // El tamaño máximo del cubo (la ráfaga máxima que permitimos). + TokenLimit = 50, + + // Con qué frecuencia se añaden nuevas fichas al cubo. + ReplenishmentPeriod = TimeSpan.FromSeconds(1), + + // Cuántas fichas se añaden en cada período. + TokensPerPeriod = 20, + + // Cuántas peticiones pueden estar en cola esperando una ficha. + QueueLimit = 1000, + + // 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 8157e76..11fd437 100644 --- a/Elecciones-Web/src/Elecciones.Worker/Worker.cs +++ b/Elecciones-Web/src/Elecciones.Worker/Worker.cs @@ -41,38 +41,54 @@ public class Worker : BackgroundService _logger.LogInformation("Iniciando sondeo periódico de resultados..."); _logger.LogInformation("-------------------------------------------------"); + int cicloContador = 0; + while (!stoppingToken.IsCancellationRequested) { + var cicloInicio = DateTime.UtcNow; + cicloContador++; + var authToken = await _apiService.GetAuthTokenAsync(); if (string.IsNullOrEmpty(authToken)) { - _logger.LogError("CRÍTICO: No se pudo obtener el token de autenticación. Reintentando en 1 minuto..."); + _logger.LogError("CRÍTICO: No se pudo obtener el token. Reintentando en 1 minuto..."); await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); continue; } - // --- CAMBIO CLAVE: DE PARALELO A SECUENCIAL --- - // Se elimina Task.WhenAll y se ejecutan las tareas una después de la otra. - // Esto previene los errores de Gateway Timeout (504). - _logger.LogInformation("--- Iniciando sondeo de Resultados Municipales ---"); + // --- CICLO CALIENTE: TAREAS DE ALTA PRIORIDAD (SIEMPRE SE EJECUTAN) --- + _logger.LogInformation("--- Iniciando Ciclo Caliente #{ciclo} ---", cicloContador); + await SondearResultadosMunicipalesAsync(authToken, stoppingToken); - - _logger.LogInformation("--- Iniciando sondeo de Resumen Provincial ---"); await SondearResumenProvincialAsync(authToken, stoppingToken); - - _logger.LogInformation("--- Iniciando sondeo de Estado de Recuento General ---"); await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); - _logger.LogInformation("--- Iniciando sondeo de Proyección de Bancas ---"); - await SondearProyeccionBancasAsync(authToken, stoppingToken); + // --- CICLO FRÍO: TAREAS DE BAJA PRIORIDAD (SE EJECUTAN CADA 5 CICLOS) --- + // El operador '%' (módulo) nos dice si el contador es divisible por 5. + if (cicloContador % 5 == 1) // Se ejecuta en el ciclo 1, 6, 11, etc. + { + _logger.LogInformation("--- Iniciando Ciclo Frío (Bancas y Telegramas) ---"); + await SondearProyeccionBancasAsync(authToken, stoppingToken); + await SondearNuevosTelegramasAsync(authToken, stoppingToken); + } - //_logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas ---"); - //await SondearNuevosTelegramasAsync(authToken, stoppingToken); + var cicloFin = DateTime.UtcNow; + var duracionCiclo = cicloFin - cicloInicio; + _logger.LogInformation("Ciclo #{ciclo} completado en {duration} segundos.", cicloContador, duracionCiclo.TotalSeconds); + + // --- ESPERA INTELIGENTE --- + // Esperamos lo que quede para completar 1 minuto desde el inicio del ciclo. + // Si el ciclo tardó 20 segundos, esperamos 40. Si tardó más de 1 minuto, la espera es mínima. + var tiempoDeEspera = TimeSpan.FromMinutes(1) - duracionCiclo; + if (tiempoDeEspera < TimeSpan.Zero) + { + tiempoDeEspera = TimeSpan.FromSeconds(5); // Una espera mínima si el ciclo se excedió + } try { - _logger.LogInformation("Ciclo de sondeo completado. Esperando 5 minutos para el siguiente..."); - await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); + _logger.LogInformation("Esperando {wait_seconds} segundos para el siguiente ciclo...", tiempoDeEspera.TotalSeconds); + await Task.Delay(tiempoDeEspera, stoppingToken); } catch (TaskCanceledException) { diff --git a/Elecciones-Web/src/Elecciones.Worker/obj/Debug/net9.0/Elecciones.Worker.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Worker/obj/Debug/net9.0/Elecciones.Worker.AssemblyInfo.cs index 469da4a..cb1bf86 100644 --- a/Elecciones-Web/src/Elecciones.Worker/obj/Debug/net9.0/Elecciones.Worker.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Worker/obj/Debug/net9.0/Elecciones.Worker.AssemblyInfo.cs @@ -14,7 +14,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+a4e47b6e3d1f8b0746f4f910f56a94e17b2e030c")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+68dce9415e165633856e4fae9b2d71cc07b4e2ff")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Worker")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Worker")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Worker/obj/Elecciones.Worker.csproj.nuget.dgspec.json b/Elecciones-Web/src/Elecciones.Worker/obj/Elecciones.Worker.csproj.nuget.dgspec.json index ee7ae44..de8fa96 100644 --- a/Elecciones-Web/src/Elecciones.Worker/obj/Elecciones.Worker.csproj.nuget.dgspec.json +++ b/Elecciones-Web/src/Elecciones.Worker/obj/Elecciones.Worker.csproj.nuget.dgspec.json @@ -203,6 +203,10 @@ "Microsoft.Extensions.Http": { "target": "Package", "version": "[9.0.8, )" + }, + "System.Threading.RateLimiting": { + "target": "Package", + "version": "[9.0.8, )" } }, "imports": [ @@ -309,6 +313,10 @@ "Serilog.Sinks.File": { "target": "Package", "version": "[7.0.0, )" + }, + "System.Threading.RateLimiting": { + "target": "Package", + "version": "[9.0.8, )" } }, "imports": [