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; /// /// EmailLog 정리 BackgroundService. /// - Sent 7일 후 삭제 /// - Failed 30일 후 삭제 /// - Pending 24시간 이상 쌓인 stale 항목은 Failed 로 전환 (Worker 다운/SMTP 장기 장애 회수) /// /// 매 24시간마다 실행 (시작 직후 30초 후 첫 실행). /// 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 _logger; private readonly IServiceScopeFactory _scopeFactory; public EmailLogJanitor(ILogger 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(); 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); } }