171 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			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);
 | |
|             });
 | |
| } |