//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(); // 2. Configura Serilog usando AddSerilog. builder.Services.AddSerilog((services, configuration) => { var loggingSwitch = services.GetRequiredService(); 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(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(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(); // Registramos el servicio de token como un Singleton para que sea compartido. builder.Services.AddSingleton(); // Registramos el servicio de configuraciones de workers como un Singleton para que sea compartido. builder.Services.AddSingleton(); // Registramos ambos workers. El framework se encargará de iniciarlos y detenerlos. builder.Services.AddHostedService(); builder.Services.AddHostedService(); 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(); var loggingSwitchService = services.GetRequiredService(); // 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 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); }); }