| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- using Domain.Entities.Common.ValueObject;
- using Infrastructure.Persistence;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Hosting;
- using Microsoft.Extensions.Logging;
- namespace MailWorker;
- /// <summary>
- /// EmailLog 정리 BackgroundService.
- /// - Sent 7일 후 삭제
- /// - Failed 30일 후 삭제
- /// - Pending 24시간 이상 쌓인 stale 항목은 Failed 로 전환 (Worker 다운/SMTP 장기 장애 회수)
- ///
- /// 매 24시간마다 실행 (시작 직후 30초 후 첫 실행).
- /// </summary>
- public sealed class EmailLogJanitor : BackgroundService
- {
- private static readonly TimeSpan FirstRunDelay = TimeSpan.FromSeconds(30);
- private static readonly TimeSpan RunInterval = TimeSpan.FromHours(24);
- private const int SentRetentionDays = 7;
- private const int FailedRetentionDays = 30;
- private const int StalePendingHours = 24;
- private readonly ILogger<EmailLogJanitor> _logger;
- private readonly IServiceScopeFactory _scopeFactory;
- public EmailLogJanitor(ILogger<EmailLogJanitor> logger, IServiceScopeFactory scopeFactory)
- {
- _logger = logger;
- _scopeFactory = scopeFactory;
- }
- protected override async Task ExecuteAsync(CancellationToken stoppingToken)
- {
- _logger.LogInformation("EmailLogJanitor started (firstRun in {Delay}, then every {Interval})", FirstRunDelay, RunInterval);
- try
- {
- await Task.Delay(FirstRunDelay, stoppingToken);
- }
- catch (OperationCanceledException)
- {
- return;
- }
- while (!stoppingToken.IsCancellationRequested)
- {
- try
- {
- await RunOnceAsync(stoppingToken);
- }
- catch (OperationCanceledException)
- {
- break;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "EmailLogJanitor cycle failed");
- }
- try
- {
- await Task.Delay(RunInterval, stoppingToken);
- }
- catch (OperationCanceledException)
- {
- break;
- }
- }
- }
- private async Task RunOnceAsync(CancellationToken ct)
- {
- using var scope = _scopeFactory.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
- var now = DateTime.UtcNow;
- var sentCutoff = now.AddDays(-SentRetentionDays);
- var failedCutoff = now.AddDays(-FailedRetentionDays);
- var staleCutoff = now.AddHours(-StalePendingHours);
- var sentDeleted = await db.EmailLog
- .Where(x => x.Status == MailStatus.Sent && x.ProcessedAt != null && x.ProcessedAt < sentCutoff)
- .ExecuteDeleteAsync(ct);
- var failedDeleted = await db.EmailLog
- .Where(x => x.Status == MailStatus.Failed && x.FailedAt != null && x.FailedAt < failedCutoff)
- .ExecuteDeleteAsync(ct);
- var staleMarked = await db.EmailLog
- .Where(x => x.Status == MailStatus.Pending && x.CreatedAt < staleCutoff)
- .ExecuteUpdateAsync(s => s
- .SetProperty(x => x.Status, MailStatus.Failed)
- .SetProperty(x => x.FailedAt, now)
- .SetProperty(x => x.LastError, "stale_in_queue"), ct);
- _logger.LogInformation("Janitor cycle: sentDeleted={SentDeleted}, failedDeleted={FailedDeleted}, staleMarked={StaleMarked}",
- sentDeleted, failedDeleted, staleMarked);
- }
- }
|