diff --git a/src/KArtSell.Host/Program.cs b/src/KArtSell.Host/Program.cs index 1a46d768..edb28712 100644 --- a/src/KArtSell.Host/Program.cs +++ b/src/KArtSell.Host/Program.cs @@ -213,115 +213,6 @@ app.UseAuthorization(); app.UseFastEndpoints(config => config.Endpoints.RoutePrefix = "api"); app.MapHub("/api/hubs/shadow-run"); -// Register model operations schedules with timeout resilience (Hangfire distributed lock may be stuck) -var logger = app.Services.GetRequiredService>(); - -try -{ - app.Services.RegisterModelOperationsSchedules(modelOperationsDispatcherEnabled, modelOperationsDispatcherCron); - logger.LogInformation("✅ Model operations schedules registered"); -} -catch (Exception ex) when (ex.Message.Contains("Timeout")) -{ - logger.LogWarning(ex, "⚠️ Hangfire lock timeout registering model operations schedules; job may be registered by another instance or lock is stuck"); -} -var hangfireRetryEnabled = Environment.GetEnvironmentVariable("HANGFIRE_RETRY_ENABLED") != "false"; - -if (hangfireRetryEnabled) -{ - try - { - RecurringJob.AddOrUpdate( - "outbox-poller", - job => job.ExecuteAsync(CancellationToken.None), - "* * * * *", - new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); - logger.LogInformation("✅ Recurring job 'outbox-poller' registered"); - } - catch (Exception ex) when (ex.Message.Contains("Timeout")) - { - logger.LogWarning(ex, "⚠️ Hangfire lock timeout registering outbox-poller; job may be registered by another instance or lock is stuck"); - } - - try - { - RecurringJob.AddOrUpdate( - "downstream-consumer", - job => job.ExecuteAsync(CancellationToken.None), - "* * * * *", - new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); - logger.LogInformation("✅ Recurring job 'downstream-consumer' registered"); - } - catch (Exception ex) when (ex.Message.Contains("Timeout")) - { - logger.LogWarning(ex, "⚠️ Hangfire lock timeout registering downstream-consumer; job may be registered by another instance or lock is stuck"); - } -} -else -{ - logger.LogInformation("⏭️ Hangfire recurring jobs skipped (HANGFIRE_RETRY_ENABLED=false)"); -} - -// OpenDart daily batch (KST timezone, market open 09:00) -var kstTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Seoul"); - -try -{ - RecurringJob.AddOrUpdate( - "opendart-daily-batch", - job => job.ExecuteAsync(CancellationToken.None), - "0 9 * * *", // 09:00 every day KST - new RecurringJobOptions { TimeZone = kstTimeZone }); - logger.LogInformation("✅ Recurring job 'opendart-daily-batch' registered"); -} -catch (Exception ex) when (ex.Message.Contains("Timeout")) -{ - logger.LogWarning(ex, "⚠️ Hangfire lock timeout registering opendart-daily-batch; job may be registered by another instance or lock is stuck"); -} - -// Recommendation report generation (KST timezone, market open 09:00) -try -{ - RecurringJob.AddOrUpdate( - "daily-recommendation", - job => job.ExecuteAsync(CancellationToken.None), - "0 9 * * *", // 09:00 every day - new RecurringJobOptions { TimeZone = kstTimeZone }); - logger.LogInformation("✅ Recurring job 'daily-recommendation' registered"); -} -catch (Exception ex) when (ex.Message.Contains("Timeout")) -{ - logger.LogWarning(ex, "⚠️ Hangfire lock timeout registering daily-recommendation; job may be registered by another instance or lock is stuck"); -} - -try -{ - RecurringJob.AddOrUpdate( - "weekly-recommendation", - job => job.ExecuteAsync(CancellationToken.None), - "0 9 * * 6", // 09:00 every Saturday - new RecurringJobOptions { TimeZone = kstTimeZone }); - logger.LogInformation("✅ Recurring job 'weekly-recommendation' registered"); -} -catch (Exception ex) when (ex.Message.Contains("Timeout")) -{ - logger.LogWarning(ex, "⚠️ Hangfire lock timeout registering weekly-recommendation; job may be registered by another instance or lock is stuck"); -} - -try -{ - RecurringJob.AddOrUpdate( - "monthly-recommendation", - job => job.ExecuteAsync(CancellationToken.None), - "0 9 1 * *", // 09:00 on the 1st of every month - new RecurringJobOptions { TimeZone = kstTimeZone }); - logger.LogInformation("✅ Recurring job 'monthly-recommendation' registered"); -} -catch (Exception ex) when (ex.Message.Contains("Timeout")) -{ - logger.LogWarning(ex, "⚠️ Hangfire lock timeout registering monthly-recommendation; job may be registered by another instance or lock is stuck"); -} - app.MapGet("/health/live", () => Results.Ok(new { status = "ok", @@ -338,7 +229,84 @@ app.MapGet("/health/ready", async (NpgsqlDataSource source, CancellationToken ct return Results.Ok(new { status = "ready", database = "reachable" }); }); -app.Run(); +// Start app in background and register Hangfire jobs after Kestrel binds +var logger = app.Services.GetRequiredService>(); +var runTask = app.RunAsync(); + +// Give Kestrel time to bind (typically < 1 second) +await Task.Delay(2000); +logger.LogInformation("📡 Kestrel binding complete, now registering Hangfire jobs in background..."); + +// Register model operations schedules with timeout (Hangfire distributed lock may be stuck) +try +{ + var scheduleTask = Task.Run(() => + { + app.Services.RegisterModelOperationsSchedules(modelOperationsDispatcherEnabled, modelOperationsDispatcherCron); + }); + + if (!scheduleTask.Wait(TimeSpan.FromSeconds(5))) + { + logger.LogWarning("⚠️ Hangfire lock timeout registering model operations schedules; continuing anyway"); + } + else + { + logger.LogInformation("✅ Model operations schedules registered"); + } +} +catch (Exception ex) +{ + logger.LogWarning(ex, "⚠️ Error registering model operations schedules; continuing anyway"); +} + +// Register recurring jobs (with general exception handling) +var hangfireRetryEnabled = Environment.GetEnvironmentVariable("HANGFIRE_RETRY_ENABLED") != "false"; +if (hangfireRetryEnabled) +{ + try + { + RecurringJob.AddOrUpdate( + "outbox-poller", + job => job.ExecuteAsync(CancellationToken.None), + "* * * * *", + new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); + logger.LogInformation("✅ Recurring job 'outbox-poller' registered"); + } + catch (Exception ex) + { + logger.LogWarning(ex, "⚠️ Hangfire error registering outbox-poller; continuing anyway"); + } + + try + { + RecurringJob.AddOrUpdate( + "downstream-consumer", + job => job.ExecuteAsync(CancellationToken.None), + "* * * * *", + new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); + logger.LogInformation("✅ Recurring job 'downstream-consumer' registered"); + } + catch (Exception ex) + { + logger.LogWarning(ex, "⚠️ Hangfire error registering downstream-consumer; continuing anyway"); + } +} +else +{ + logger.LogInformation("⏭️ Hangfire recurring jobs skipped"); +} + +// Register other Hangfire jobs +var kstTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Seoul"); +try { RecurringJob.AddOrUpdate("opendart-daily-batch", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ opendart-daily-batch registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ opendart-daily-batch error"); } +try { RecurringJob.AddOrUpdate("daily-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ daily-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ daily-recommendation error"); } +try { RecurringJob.AddOrUpdate("weekly-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * 6", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ weekly-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ weekly-recommendation error"); } +try { RecurringJob.AddOrUpdate("monthly-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 1 * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ monthly-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ monthly-recommendation error"); } + +logger.LogInformation("🎯 Hangfire background jobs initialization complete"); + +// Wait for app to run +await runTask; /// /// Resolve secrets from environment variables, handling placeholders like ${VAR_NAME}.