feat: DataRetentionWorker 구현 (#152)
All checks were successful
SPMS_API/pipeline/head This commit looks good
All checks were successful
SPMS_API/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/153
This commit is contained in:
commit
fec1bf289f
|
|
@ -84,6 +84,7 @@ public static class DependencyInjection
|
||||||
services.AddHostedService<ScheduleWorker>();
|
services.AddHostedService<ScheduleWorker>();
|
||||||
services.AddHostedService<DailyStatWorker>();
|
services.AddHostedService<DailyStatWorker>();
|
||||||
services.AddHostedService<DeadTokenCleanupWorker>();
|
services.AddHostedService<DeadTokenCleanupWorker>();
|
||||||
|
services.AddHostedService<DataRetentionWorker>();
|
||||||
|
|
||||||
// Token Store & Email Service
|
// Token Store & Email Service
|
||||||
services.AddMemoryCache();
|
services.AddMemoryCache();
|
||||||
|
|
|
||||||
124
SPMS.Infrastructure/Workers/DataRetentionWorker.cs
Normal file
124
SPMS.Infrastructure/Workers/DataRetentionWorker.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SPMS.Domain.Entities;
|
||||||
|
using SPMS.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
namespace SPMS.Infrastructure.Workers;
|
||||||
|
|
||||||
|
public class DataRetentionWorker : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<DataRetentionWorker> _logger;
|
||||||
|
|
||||||
|
private static readonly TimeZoneInfo KstZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Seoul");
|
||||||
|
private const int BatchSize = 10000;
|
||||||
|
|
||||||
|
private static readonly (string TableName, string DateColumn, int RetentionDays)[] RetentionPolicies =
|
||||||
|
{
|
||||||
|
("PushSendLog", "SentAt", 90),
|
||||||
|
("PushOpenLog", "OpenedAt", 90),
|
||||||
|
("WebhookLog", "SentAt", 30),
|
||||||
|
("SystemLog", "CreatedAt", 180)
|
||||||
|
};
|
||||||
|
|
||||||
|
public DataRetentionWorker(
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
ILogger<DataRetentionWorker> logger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("DataRetentionWorker 시작");
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var delay = CalculateDelayUntilNextRun();
|
||||||
|
_logger.LogInformation("DataRetentionWorker 다음 실행까지 {Delay} 대기", delay);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, stoppingToken);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await PurgeAsync(stoppingToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "DataRetentionWorker 정리 중 오류 발생");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeSpan CalculateDelayUntilNextRun()
|
||||||
|
{
|
||||||
|
var nowKst = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, KstZone);
|
||||||
|
var nextRun = nowKst.Date.AddHours(4);
|
||||||
|
|
||||||
|
if (nowKst >= nextRun)
|
||||||
|
nextRun = nextRun.AddDays(1);
|
||||||
|
|
||||||
|
return nextRun - nowKst;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PurgeAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
|
||||||
|
_logger.LogInformation("DataRetentionWorker 정리 시작");
|
||||||
|
|
||||||
|
var totalDeletedAll = 0;
|
||||||
|
|
||||||
|
foreach (var (tableName, dateColumn, retentionDays) in RetentionPolicies)
|
||||||
|
{
|
||||||
|
if (stoppingToken.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
var cutoffUtc = DateTime.UtcNow.AddDays(-retentionDays);
|
||||||
|
var totalDeleted = 0;
|
||||||
|
int deletedInBatch;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var sql = $"DELETE FROM `{tableName}` WHERE `{dateColumn}` < @p0 LIMIT {BatchSize}";
|
||||||
|
deletedInBatch = await context.Database
|
||||||
|
.ExecuteSqlRawAsync(sql, new[] { cutoffUtc }, stoppingToken);
|
||||||
|
|
||||||
|
totalDeleted += deletedInBatch;
|
||||||
|
|
||||||
|
if (deletedInBatch > 0)
|
||||||
|
_logger.LogInformation(
|
||||||
|
"DataRetentionWorker [{Table}] 배치 삭제: {BatchCount}건 (누적: {TotalDeleted}건)",
|
||||||
|
tableName, deletedInBatch, totalDeleted);
|
||||||
|
} while (deletedInBatch > 0 && !stoppingToken.IsCancellationRequested);
|
||||||
|
|
||||||
|
if (totalDeleted > 0)
|
||||||
|
_logger.LogInformation(
|
||||||
|
"DataRetentionWorker [{Table}] 정리 완료: {TotalDeleted}건 삭제 (보관 기간: {Days}일)",
|
||||||
|
tableName, totalDeleted, retentionDays);
|
||||||
|
|
||||||
|
totalDeletedAll += totalDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemLog에 정리 완료 기록
|
||||||
|
context.Set<SystemLog>().Add(new SystemLog
|
||||||
|
{
|
||||||
|
Action = "DataRetention",
|
||||||
|
Details = $"데이터 보관 주기 정리 완료: 총 {totalDeletedAll}건 삭제",
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync(stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("DataRetentionWorker 전체 정리 완료: 총 {TotalDeleted}건 삭제", totalDeletedAll);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user