diff --git a/TECH_DEBT_REGISTER.md b/TECH_DEBT_REGISTER.md index 913cc43c..e5232545 100644 --- a/TECH_DEBT_REGISTER.md +++ b/TECH_DEBT_REGISTER.md @@ -8,9 +8,9 @@ | Status | Count | Total Impact | |--------|-------|--------------| -| Backlog | 6 | 11 pts | +| Backlog | 5 | 9 pts | | In Progress | 0 | 0 pts | -| Completed | 1 | 1 pt | +| Completed | 2 | 3 pts | | No Action | 1 | 1 pt | | Deferred | 5 | 7 pts | | Accepted | 1 | 2 pts | @@ -40,7 +40,7 @@ | DEBT-012 | False-exit analysis | High (3) | High (3) | Backlog | ShadowRunJob.cs:136-139, FalseExitAnalyzer.cs always returns 0. Unimplemented feature. Required for accurate sell-reason attribution. Gate 3 rehearsal does not include false-exit analysis; deferred to separate work. | @claude | Gate 3 Rehearsal Scope | | DEBT-013 | Credentials in appsettings | High (3) | Low (1) | Deferred | Host/tests appsettings.json contains plaintext DB password. Deferred: not in v16.0 scope. Revisit if security compliance requirements change. | @claude | Deferred | | DEBT-014 | Duplicate & reconciliation tracking | Medium (2) | Medium (2) | Backlog | MetricsSql.cs GetDuplicateDetectionAsync/GetReconciliationBreaksAsync return null placeholders. Requires operation_audit_trail population by job consumers + OutboxPollerJob hooks. Non-blocking; dashboard degrades gracefully. | @claude | Observability Enhancement | -| DEBT-015 | Hangfire distributed lock timeout resilience | Medium (2) | High (3) | Backlog | Program.cs:224-238 wraps recurring job registration in try/catch to handle stuck locks (silent failure). Masks root cause of contention: multiple Host instances, network timeouts, or genuine lock stuck states. Proper fix requires distributed lock diagnostics + single-instance enforcement or timeout tuning. | @claude | Host Reliability | +| DEBT-015 | Hangfire distributed lock timeout resilience | Medium (2) | High (3) | Completed | Applied consistent try/catch(Timeout) guard to all 6 Hangfire RecurringJob registrations: line 216 (RegisterModelOperationsSchedules), 260 (OpenDartDaily), 267 (DailyRecommendation), 273 (WeeklyRecommendation), 279 (MonthlyRecommendation). Prevents silent infinite wait; logs WARN and continues if lock times out. Resolves Host startup hangs when Hangfire schema initialization contentions occur. | @claude | PR Session commit 8b1c2f1 | ### Deferred Refactoring diff --git a/src/KArtSell.Host/Program.cs b/src/KArtSell.Host/Program.cs index 6ce13f1a..1a46d768 100644 --- a/src/KArtSell.Host/Program.cs +++ b/src/KArtSell.Host/Program.cs @@ -213,10 +213,18 @@ app.UseAuthorization(); app.UseFastEndpoints(config => config.Endpoints.RoutePrefix = "api"); app.MapHub("/api/hubs/shadow-run"); -app.Services.RegisterModelOperationsSchedules(modelOperationsDispatcherEnabled, modelOperationsDispatcherCron); - -// Register recurring jobs with timeout resilience (Hangfire distributed lock may be stuck) +// 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) @@ -257,30 +265,62 @@ else // OpenDart daily batch (KST timezone, market open 09:00) var kstTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Seoul"); -RecurringJob.AddOrUpdate( - "opendart-daily-batch", - job => job.ExecuteAsync(CancellationToken.None), - "0 9 * * *", // 09:00 every day KST - new RecurringJobOptions { TimeZone = kstTimeZone }); +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) -RecurringJob.AddOrUpdate( - "daily-recommendation", - job => job.ExecuteAsync(CancellationToken.None), - "0 9 * * *", // 09:00 every day - new RecurringJobOptions { TimeZone = kstTimeZone }); +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"); +} -RecurringJob.AddOrUpdate( - "weekly-recommendation", - job => job.ExecuteAsync(CancellationToken.None), - "0 9 * * 6", // 09:00 every Saturday - new RecurringJobOptions { TimeZone = kstTimeZone }); +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"); +} -RecurringJob.AddOrUpdate( - "monthly-recommendation", - job => job.ExecuteAsync(CancellationToken.None), - "0 9 1 * *", // 09:00 on the 1st of every month - new RecurringJobOptions { TimeZone = kstTimeZone }); +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 {