using Database.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Esim.SendMail;
///
/// Background worker to clean up expired messages from MESSAGE_QUEUE.
/// Messages older than configured expiration days are moved to MESSAGE_QUEUE_HIS with failed status and deleted.
///
public class ExpiredMessageCleanupWorker : BackgroundService
{
private readonly ILogger _logger;
private readonly IServiceProvider _serviceProvider;
private readonly int _intervalMinutes;
private readonly int _expirationDays;
// Message statuses
private const int STATUS_PENDING = 0;
private const int STATUS_PROCESSING = 1;
private const int STATUS_FAILED = 3;
public ExpiredMessageCleanupWorker(
ILogger logger,
IServiceProvider serviceProvider,
IConfiguration configuration)
{
_logger = logger;
_serviceProvider = serviceProvider;
_intervalMinutes = int.Parse(configuration["Cleanup:IntervalMinutes"] ?? "60"); // Default: 1 hour
_expirationDays = int.Parse(configuration["Cleanup:ExpirationDays"] ?? "1"); // Default: 1 day
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("ExpiredMessageCleanupWorker started. Interval: {Interval} minutes, Expiration: {Days} day(s)",
_intervalMinutes, _expirationDays);
// Wait a bit for initialization
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await CleanupExpiredMessagesAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in cleanup loop");
}
try
{
await Task.Delay(TimeSpan.FromMinutes(_intervalMinutes), stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
}
_logger.LogInformation("ExpiredMessageCleanupWorker stopped.");
}
private async Task CleanupExpiredMessagesAsync(CancellationToken stoppingToken)
{
var startTime = DateTime.Now;
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService();
try
{
// Find messages older than expiration days that are still pending or processing
var expirationDate = DateTime.Now.AddDays(-_expirationDays);
var expiredMessages = await dbContext.MessageQueues
.Where(m => m.CreatedDate <= expirationDate
&& (m.Status == STATUS_PENDING || m.Status == STATUS_PROCESSING))
.OrderBy(m => m.CreatedDate)
.ToListAsync(stoppingToken);
if (!expiredMessages.Any())
{
_logger.LogDebug("No expired messages found");
return;
}
_logger.LogWarning("Found {Count} expired messages (older than {Days} day(s)). Moving to history and deleting...",
expiredMessages.Count, _expirationDays);
// Log sample of expired messages for audit
var sampleSize = Math.Min(5, expiredMessages.Count);
for (int i = 0; i < sampleSize; i++)
{
var msg = expiredMessages[i];
var age = DateTime.Now - (msg.CreatedDate ?? DateTime.Now);
var subjectPreview = msg.Subject != null
? msg.Subject.Substring(0, Math.Min(50, msg.Subject.Length))
: "(no subject)";
_logger.LogInformation(
"Expired message sample: ID={Id}, Recipient={Recipient}, Subject={Subject}, Age={Age:F1}h, Status={Status}, Retries={Retry}/{Max}",
msg.Id, msg.Recipient, subjectPreview,
age.TotalHours, msg.Status, msg.RetryCount ?? 0, msg.MaxRetry ?? 3);
}
if (expiredMessages.Count > sampleSize)
{
_logger.LogInformation("... and {More} more expired messages", expiredMessages.Count - sampleSize);
}
// Move to MESSAGE_QUEUE_HIS with failed status
await MoveToHistoryAsync(dbContext, expiredMessages, stoppingToken);
// Delete from MESSAGE_QUEUE
var ids = expiredMessages.Select(m => m.Id).ToList();
var deleteSql = $"DELETE FROM MESSAGE_QUEUE WHERE ID IN ({string.Join(",", ids)})";
await dbContext.Database.ExecuteSqlRawAsync(deleteSql, stoppingToken);
var elapsed = DateTime.Now - startTime;
_logger.LogInformation("Successfully cleaned up {Count} expired messages in {Elapsed:F0}ms. Messages archived to MESSAGE_QUEUE_HIS with FAILED status.",
expiredMessages.Count, elapsed.TotalMilliseconds);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error cleaning up expired messages");
}
}
private async Task MoveToHistoryAsync(
ModelContext dbContext,
List messages,
CancellationToken stoppingToken)
{
if (!messages.Any()) return;
try
{
var ids = string.Join(",", messages.Select(m => m.Id));
// Insert to history with STATUS = 3 (FAILED - EXPIRED)
var insertSql = $@"
INSERT INTO MESSAGE_QUEUE_HIS
(ID, MESSAGE_TYPE, RECIPIENT, SUBJECT, CONTENT, TEMPLATE_CODE, TEMPLATE_DATA,
PRIORITY, STATUS, SCHEDULED_AT, PROCESSED_AT, RETRY_COUNT, MAX_RETRY,
ERROR_MESSAGE, CREATED_BY, CREATED_DATE, MOVED_DATE)
SELECT
ID, MESSAGE_TYPE, RECIPIENT, SUBJECT, CONTENT, TEMPLATE_CODE, TEMPLATE_DATA,
PRIORITY, {STATUS_FAILED} AS STATUS, SCHEDULED_AT, SYSDATE AS PROCESSED_AT, RETRY_COUNT, MAX_RETRY,
'EXPIRED: Message older than {_expirationDays} day(s) - Auto-deleted to prevent spam and table bloat. Created: ' || TO_CHAR(CREATED_DATE, 'YYYY-MM-DD HH24:MI:SS') AS ERROR_MESSAGE,
CREATED_BY, CREATED_DATE, SYSDATE
FROM MESSAGE_QUEUE
WHERE ID IN ({ids})";
await dbContext.Database.ExecuteSqlRawAsync(insertSql, stoppingToken);
_logger.LogDebug("Moved {Count} expired messages to MESSAGE_QUEUE_HIS with FAILED status", messages.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to move expired messages to history");
throw; // Re-throw to prevent deletion if archival fails
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("ExpiredMessageCleanupWorker stopping...");
await base.StopAsync(cancellationToken);
_logger.LogInformation("ExpiredMessageCleanupWorker stopped gracefully");
}
}