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);
}
}