using Distribution.Infra.UoW; namespace Distribution.Features.Notifications; /// /// Daily retention purge: deletes read inbox rows older than Notifications:RetentionDays /// (default 90), then any notification left with no recipients. /// public class NotificationCleanupJob( IServiceScopeFactory scopes, IConfiguration config, ILogger logger) : BackgroundService { private const int BatchSize = 500; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Let the app finish starting before the first run. await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); while (!stoppingToken.IsCancellationRequested) { try { await RunOnceAsync(stoppingToken); } catch (OperationCanceledException) { break; } catch (Exception ex) { logger.LogError(ex, "Notification cleanup failed."); } await Task.Delay(TimeSpan.FromHours(24), stoppingToken); } } private async Task RunOnceAsync(CancellationToken ct) { var retentionDays = config.GetValue("Notifications:RetentionDays", 90); var cutoff = DateTime.Now.AddDays(-retentionDays); using var scope = scopes.CreateScope(); var repo = scope.ServiceProvider.GetRequiredService(); var uow = scope.ServiceProvider.GetRequiredService(); var removed = 0; while (true) { var expired = await repo.GetExpiredReadRecipientsAsync(cutoff, BatchSize); if (expired.Count == 0) break; repo.RemoveRecipients(expired); await uow.CommitAsync(ct); removed += expired.Count; } while (true) { var orphans = await repo.GetOrphanNotificationsAsync(BatchSize); if (orphans.Count == 0) break; repo.RemoveNotifications(orphans); await uow.CommitAsync(ct); } if (removed > 0) logger.LogInformation("Notification cleanup removed {Count} read inbox rows older than {Days} days.", removed, retentionDays); } }