Files
Elecciones-2025/Elecciones-Web/src/Elecciones.Worker/Program.cs
2025-09-06 21:56:29 -03:00

171 lines
6.3 KiB
C#

//Elecciones.Worker/Program.cs
using Elecciones.Database;
using Elecciones.Infrastructure.Services;
using Elecciones.Worker;
using Microsoft.EntityFrameworkCore;
using System.Net;
using Serilog;
using System.Net.Security;
using System.Security.Authentication;
using Polly;
using Polly.Extensions.Http;
using System.Threading.RateLimiting;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
Log.Information("Iniciando Elecciones.Worker Host...");
var builder = Host.CreateApplicationBuilder(args);
// 1. Registra el servicio del interruptor como siempre.
builder.Services.AddSingleton<LoggingSwitchService>();
// 2. Configura Serilog usando AddSerilog.
builder.Services.AddSerilog((services, configuration) => {
var loggingSwitch = services.GetRequiredService<LoggingSwitchService>();
configuration
.ReadFrom.Configuration(builder.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.MinimumLevel.ControlledBy(loggingSwitch.LevelSwitch)
.WriteTo.Console()
.WriteTo.File("logs/worker-.log", rollingInterval: RollingInterval.Day);
});
// --- Configuración de Servicios ---
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<EleccionesDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddHttpClient("ElectoralApiClient", client =>
{
var baseUrl = builder.Configuration["ElectoralApi:BaseUrl"];
if (!string.IsNullOrEmpty(baseUrl))
{
client.BaseAddress = new Uri(baseUrl);
}
// --- TIMEOUT MÁS LARGO ---
// Aumentamos el tiempo de espera a 90 segundos.
// Esto le dará a las peticiones lentas de la API tiempo suficiente para responder.
client.Timeout = TimeSpan.FromSeconds(90);
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
client.DefaultRequestHeaders.Add("Accept", "*/*");
client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
client.DefaultRequestHeaders.Add("Connection", "keep-alive");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new SocketsHttpHandler
{
// Habilita la descompresión automática de respuestas GZIP y Deflate.
// Esto soluciona el error de JsonException ('0x1F').
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
SslOptions = new SslClientAuthenticationOptions
{
EnabledSslProtocols = SslProtocols.Tls13,
}
};
if (!OperatingSystem.IsWindows())
{
handler.SslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(new[]
{
TlsCipherSuite.TLS_AES_128_GCM_SHA256,
TlsCipherSuite.TLS_AES_256_GCM_SHA384,
TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256
});
}
return handler;
})
.AddPolicyHandler(GetRetryPolicy());
// --- LIMITADOR DE VELOCIDAD BASADO EN TOKEN BUCKET ---
builder.Services.AddSingleton<RateLimiter>(sp =>
new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
TokenLimit = 50, // Ráfaga máxima permitida
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
TokensPerPeriod = 20, // Ritmo de recarga: 20 peticiones por segundo
QueueLimit = 1000,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst
}));
builder.Services.AddScoped<IElectoralApiService, ElectoralApiService>();
// Registramos el servicio de token como un Singleton para que sea compartido.
builder.Services.AddSingleton<SharedTokenService>();
// Registramos el servicio de configuraciones de workers como un Singleton para que sea compartido.
builder.Services.AddSingleton<WorkerConfigService>();
// Registramos ambos workers. El framework se encargará de iniciarlos y detenerlos.
builder.Services.AddHostedService<CriticalDataWorker>();
builder.Services.AddHostedService<LowPriorityDataWorker>();
var host = builder.Build();
// --- LÓGICA PARA LEER EL NIVEL DE LOGGING AL INICIO ---
// Creamos un scope temporal para leer la configuración de la BD
using (var scope = builder.Services.BuildServiceProvider().CreateScope())
{
var services = scope.ServiceProvider;
try
{
var dbContext = services.GetRequiredService<EleccionesDbContext>();
var loggingSwitchService = services.GetRequiredService<LoggingSwitchService>();
// Buscamos el nivel de logging guardado en la BD
var logLevelConfig = await dbContext.Configuraciones
.AsNoTracking()
.FirstOrDefaultAsync(c => c.Clave == "Logging_Level");
if (logLevelConfig != null)
{
// Si lo encontramos, lo aplicamos al interruptor
loggingSwitchService.SetLoggingLevel(logLevelConfig.Valor);
Console.WriteLine($"--> Nivel de logging inicial establecido desde la BD a: {logLevelConfig.Valor}");
}
}
catch (Exception ex)
{
// Si hay un error (ej. la BD no está disponible al arrancar), se usará el nivel por defecto 'Information'.
Console.WriteLine($"--> No se pudo establecer el nivel de logging desde la BD: {ex.Message}");
}
}
try
{
host.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "El Host de Elecciones.Worker terminó inesperadamente");
}
finally
{
Log.CloseAndFlush();
}
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
// Manejar peticiones que fallaron por errores de red O que devolvieron un error de servidor (como 504)
.HandleTransientHttpError()
// O que devolvieron un código 504 Gateway Timeout específicamente
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.GatewayTimeout)
// Esperar y reintentar. La espera se duplica en cada reintento.
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryAttempt, context) =>
{
// Opcional: Loguear cada reintento. Necesitarías pasar ILogger aquí.
// Log.Warning("Retrying due to {StatusCode}. Waited {seconds}s. Attempt {retryAttempt}/3", outcome.Result.StatusCode, timespan.TotalSeconds, retryAttempt);
});
}