Feat Rate Limit para cuotear peticiones.
This commit is contained in:
@@ -14,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[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.AssemblyProductAttribute("Elecciones.Api")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -306,6 +306,10 @@
|
|||||||
"Microsoft.Extensions.Http": {
|
"Microsoft.Extensions.Http": {
|
||||||
"target": "Package",
|
"target": "Package",
|
||||||
"version": "[9.0.8, )"
|
"version": "[9.0.8, )"
|
||||||
|
},
|
||||||
|
"System.Threading.RateLimiting": {
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[9.0.8, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"imports": [
|
"imports": [
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.8" />
|
||||||
|
<PackageReference Include="System.Threading.RateLimiting" Version="9.0.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Text.Json;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using static Elecciones.Core.DTOs.BancaDto;
|
using static Elecciones.Core.DTOs.BancaDto;
|
||||||
|
using System.Threading.RateLimiting;
|
||||||
|
|
||||||
namespace Elecciones.Infrastructure.Services;
|
namespace Elecciones.Infrastructure.Services;
|
||||||
|
|
||||||
@@ -15,17 +16,27 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<ElectoralApiService> _logger;
|
private readonly ILogger<ElectoralApiService> _logger;
|
||||||
|
private readonly RateLimiter _rateLimiter;
|
||||||
|
|
||||||
public ElectoralApiService(IHttpClientFactory httpClientFactory,
|
public ElectoralApiService(IHttpClientFactory httpClientFactory,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<ElectoralApiService> logger)
|
ILogger<ElectoralApiService> logger,
|
||||||
|
RateLimiter rateLimiter)
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_rateLimiter = rateLimiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> GetAuthTokenAsync()
|
public async Task<string?> 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 client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
var username = _configuration["ElectoralApi:Username"];
|
var username = _configuration["ElectoralApi:Username"];
|
||||||
@@ -39,8 +50,18 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>();
|
var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>();
|
||||||
return (tokenResponse is { Success: true, Data.AccessToken: not null }) ? tokenResponse.Data.AccessToken : null;
|
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<CatalogoDto?> GetCatalogoAmbitosAsync(string authToken, int categoriaId)
|
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 client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, $"/api/catalogo/getCatalogo?categoriaId={categoriaId}");
|
var request = new HttpRequestMessage(HttpMethod.Get, $"/api/catalogo/getCatalogo?categoriaId={categoriaId}");
|
||||||
@@ -48,8 +69,18 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<CatalogoDto>() : null;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<AgrupacionDto>?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId)
|
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 client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
var requestUri = $"/api/catalogo/getAgrupaciones?distritoId={distritoId}&categoriaId={categoriaId}";
|
var requestUri = $"/api/catalogo/getAgrupaciones?distritoId={distritoId}&categoriaId={categoriaId}";
|
||||||
@@ -58,8 +89,18 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<AgrupacionDto>>() : null;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string seccionId, string? municipioId, int categoriaId)
|
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");
|
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
|
|
||||||
@@ -77,8 +118,18 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<ResultadosDto>() : null;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string? seccionProvincialId, int categoriaId)
|
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 client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={categoriaId}";
|
var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={categoriaId}";
|
||||||
@@ -127,8 +178,18 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
// Si la respuesta fue 200 OK pero con cuerpo vacío, o si fue un error HTTP, devolvemos null.
|
// Si la respuesta fue 200 OK pero con cuerpo vacío, o si fue un error HTTP, devolvemos null.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<string>?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = 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 client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
var requestUri = $"/api/resultados/getTelegramasTotalizados?distritoId={distritoId}&seccionId={seccionId}";
|
var requestUri = $"/api/resultados/getTelegramasTotalizados?distritoId={distritoId}&seccionId={seccionId}";
|
||||||
@@ -145,8 +206,18 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
// Ahora deserializamos al tipo correcto: List<string>
|
// Ahora deserializamos al tipo correcto: List<string>
|
||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<string>>() : null;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TelegramaFileDto?> GetTelegramaFileAsync(string authToken, string mesaId)
|
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 client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
var requestUri = $"/api/resultados/getFile?mesaId={mesaId}";
|
var requestUri = $"/api/resultados/getFile?mesaId={mesaId}";
|
||||||
@@ -186,8 +257,18 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ResumenDto?> GetResumenAsync(string authToken, string distritoId)
|
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 client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
var requestUri = $"/api/resultados/getResumen?distritoId={distritoId}";
|
var requestUri = $"/api/resultados/getResumen?distritoId={distritoId}";
|
||||||
@@ -196,8 +277,18 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<ResumenDto>() : null;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<EstadoRecuentoGeneralDto?> GetEstadoRecuentoGeneralAsync(string authToken, string distritoId, int categoriaId)
|
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");
|
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
// La URL ahora usa el parámetro 'categoriaId' que se recibe
|
// La URL ahora usa el parámetro 'categoriaId' que se recibe
|
||||||
@@ -207,8 +298,18 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<EstadoRecuentoGeneralDto>() : null;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<CategoriaDto>?> GetCategoriasAsync(string authToken)
|
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 client = _httpClientFactory.CreateClient("ElectoralApiClient");
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "/api/catalogo/getCategorias");
|
var request = new HttpRequestMessage(HttpMethod.Get, "/api/catalogo/getCategorias");
|
||||||
@@ -216,4 +317,7 @@ public class ElectoralApiService : IElectoralApiService
|
|||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<CategoriaDto>>() : null;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[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.AssemblyProductAttribute("Elecciones.Infrastructure")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -126,6 +126,10 @@
|
|||||||
"Microsoft.Extensions.Http": {
|
"Microsoft.Extensions.Http": {
|
||||||
"target": "Package",
|
"target": "Package",
|
||||||
"version": "[9.0.8, )"
|
"version": "[9.0.8, )"
|
||||||
|
},
|
||||||
|
"System.Threading.RateLimiting": {
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[9.0.8, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"imports": [
|
"imports": [
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
|
<PackageReference Include="System.Threading.RateLimiting" Version="9.0.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using System.Net.Security;
|
|||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using Polly;
|
using Polly;
|
||||||
using Polly.Extensions.Http;
|
using Polly.Extensions.Http;
|
||||||
|
using System.Threading.RateLimiting;
|
||||||
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
.WriteTo.Console()
|
.WriteTo.Console()
|
||||||
@@ -81,6 +82,26 @@ builder.Services.AddHttpClient("ElectoralApiClient", client =>
|
|||||||
|
|
||||||
.AddPolicyHandler(GetRetryPolicy());
|
.AddPolicyHandler(GetRetryPolicy());
|
||||||
|
|
||||||
|
// --- LIMITADOR DE VELOCIDAD BASADO EN TOKEN BUCKET ---
|
||||||
|
builder.Services.AddSingleton<RateLimiter>(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<IElectoralApiService, ElectoralApiService>();
|
builder.Services.AddScoped<IElectoralApiService, ElectoralApiService>();
|
||||||
|
|
||||||
builder.Services.AddHostedService<Worker>();
|
builder.Services.AddHostedService<Worker>();
|
||||||
|
|||||||
@@ -41,38 +41,54 @@ public class Worker : BackgroundService
|
|||||||
_logger.LogInformation("Iniciando sondeo periódico de resultados...");
|
_logger.LogInformation("Iniciando sondeo periódico de resultados...");
|
||||||
_logger.LogInformation("-------------------------------------------------");
|
_logger.LogInformation("-------------------------------------------------");
|
||||||
|
|
||||||
|
int cicloContador = 0;
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
var cicloInicio = DateTime.UtcNow;
|
||||||
|
cicloContador++;
|
||||||
|
|
||||||
var authToken = await _apiService.GetAuthTokenAsync();
|
var authToken = await _apiService.GetAuthTokenAsync();
|
||||||
if (string.IsNullOrEmpty(authToken))
|
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);
|
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CAMBIO CLAVE: DE PARALELO A SECUENCIAL ---
|
// --- CICLO CALIENTE: TAREAS DE ALTA PRIORIDAD (SIEMPRE SE EJECUTAN) ---
|
||||||
// Se elimina Task.WhenAll y se ejecutan las tareas una después de la otra.
|
_logger.LogInformation("--- Iniciando Ciclo Caliente #{ciclo} ---", cicloContador);
|
||||||
// Esto previene los errores de Gateway Timeout (504).
|
|
||||||
_logger.LogInformation("--- Iniciando sondeo de Resultados Municipales ---");
|
|
||||||
await SondearResultadosMunicipalesAsync(authToken, stoppingToken);
|
await SondearResultadosMunicipalesAsync(authToken, stoppingToken);
|
||||||
|
|
||||||
_logger.LogInformation("--- Iniciando sondeo de Resumen Provincial ---");
|
|
||||||
await SondearResumenProvincialAsync(authToken, stoppingToken);
|
await SondearResumenProvincialAsync(authToken, stoppingToken);
|
||||||
|
|
||||||
_logger.LogInformation("--- Iniciando sondeo de Estado de Recuento General ---");
|
|
||||||
await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken);
|
await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken);
|
||||||
|
|
||||||
_logger.LogInformation("--- Iniciando sondeo de Proyección de Bancas ---");
|
// --- 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 SondearProyeccionBancasAsync(authToken, stoppingToken);
|
||||||
|
await SondearNuevosTelegramasAsync(authToken, stoppingToken);
|
||||||
|
}
|
||||||
|
|
||||||
//_logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas ---");
|
var cicloFin = DateTime.UtcNow;
|
||||||
//await SondearNuevosTelegramasAsync(authToken, stoppingToken);
|
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
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Ciclo de sondeo completado. Esperando 5 minutos para el siguiente...");
|
_logger.LogInformation("Esperando {wait_seconds} segundos para el siguiente ciclo...", tiempoDeEspera.TotalSeconds);
|
||||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
await Task.Delay(tiempoDeEspera, stoppingToken);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[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.AssemblyProductAttribute("Elecciones.Worker")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Worker")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Worker")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -203,6 +203,10 @@
|
|||||||
"Microsoft.Extensions.Http": {
|
"Microsoft.Extensions.Http": {
|
||||||
"target": "Package",
|
"target": "Package",
|
||||||
"version": "[9.0.8, )"
|
"version": "[9.0.8, )"
|
||||||
|
},
|
||||||
|
"System.Threading.RateLimiting": {
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[9.0.8, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"imports": [
|
"imports": [
|
||||||
@@ -309,6 +313,10 @@
|
|||||||
"Serilog.Sinks.File": {
|
"Serilog.Sinks.File": {
|
||||||
"target": "Package",
|
"target": "Package",
|
||||||
"version": "[7.0.0, )"
|
"version": "[7.0.0, )"
|
||||||
|
},
|
||||||
|
"System.Threading.RateLimiting": {
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[9.0.8, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"imports": [
|
"imports": [
|
||||||
|
|||||||
Reference in New Issue
Block a user