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": [