EmailLogJanitor.cs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. using Domain.Entities.Common.ValueObject;
  2. using Infrastructure.Persistence;
  3. using Microsoft.EntityFrameworkCore;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Microsoft.Extensions.Hosting;
  6. using Microsoft.Extensions.Logging;
  7. namespace MailWorker;
  8. /// <summary>
  9. /// EmailLog 정리 BackgroundService.
  10. /// - Sent 7일 후 삭제
  11. /// - Failed 30일 후 삭제
  12. /// - Pending 24시간 이상 쌓인 stale 항목은 Failed 로 전환 (Worker 다운/SMTP 장기 장애 회수)
  13. ///
  14. /// 매 24시간마다 실행 (시작 직후 30초 후 첫 실행).
  15. /// </summary>
  16. public sealed class EmailLogJanitor : BackgroundService
  17. {
  18. private static readonly TimeSpan FirstRunDelay = TimeSpan.FromSeconds(30);
  19. private static readonly TimeSpan RunInterval = TimeSpan.FromHours(24);
  20. private const int SentRetentionDays = 7;
  21. private const int FailedRetentionDays = 30;
  22. private const int StalePendingHours = 24;
  23. private readonly ILogger<EmailLogJanitor> _logger;
  24. private readonly IServiceScopeFactory _scopeFactory;
  25. public EmailLogJanitor(ILogger<EmailLogJanitor> logger, IServiceScopeFactory scopeFactory)
  26. {
  27. _logger = logger;
  28. _scopeFactory = scopeFactory;
  29. }
  30. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  31. {
  32. _logger.LogInformation("EmailLogJanitor started (firstRun in {Delay}, then every {Interval})", FirstRunDelay, RunInterval);
  33. try
  34. {
  35. await Task.Delay(FirstRunDelay, stoppingToken);
  36. }
  37. catch (OperationCanceledException)
  38. {
  39. return;
  40. }
  41. while (!stoppingToken.IsCancellationRequested)
  42. {
  43. try
  44. {
  45. await RunOnceAsync(stoppingToken);
  46. }
  47. catch (OperationCanceledException)
  48. {
  49. break;
  50. }
  51. catch (Exception ex)
  52. {
  53. _logger.LogError(ex, "EmailLogJanitor cycle failed");
  54. }
  55. try
  56. {
  57. await Task.Delay(RunInterval, stoppingToken);
  58. }
  59. catch (OperationCanceledException)
  60. {
  61. break;
  62. }
  63. }
  64. }
  65. private async Task RunOnceAsync(CancellationToken ct)
  66. {
  67. using var scope = _scopeFactory.CreateScope();
  68. var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
  69. var now = DateTime.UtcNow;
  70. var sentCutoff = now.AddDays(-SentRetentionDays);
  71. var failedCutoff = now.AddDays(-FailedRetentionDays);
  72. var staleCutoff = now.AddHours(-StalePendingHours);
  73. var sentDeleted = await db.EmailLog
  74. .Where(x => x.Status == MailStatus.Sent && x.ProcessedAt != null && x.ProcessedAt < sentCutoff)
  75. .ExecuteDeleteAsync(ct);
  76. var failedDeleted = await db.EmailLog
  77. .Where(x => x.Status == MailStatus.Failed && x.FailedAt != null && x.FailedAt < failedCutoff)
  78. .ExecuteDeleteAsync(ct);
  79. var staleMarked = await db.EmailLog
  80. .Where(x => x.Status == MailStatus.Pending && x.CreatedAt < staleCutoff)
  81. .ExecuteUpdateAsync(s => s
  82. .SetProperty(x => x.Status, MailStatus.Failed)
  83. .SetProperty(x => x.FailedAt, now)
  84. .SetProperty(x => x.LastError, "stale_in_queue"), ct);
  85. _logger.LogInformation("Janitor cycle: sentDeleted={SentDeleted}, failedDeleted={FailedDeleted}, staleMarked={StaleMarked}",
  86. sentDeleted, failedDeleted, staleMarked);
  87. }
  88. }