| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- using Database.Database;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.Logging;
- namespace Esim.SendMail;
- /// <summary>
- /// 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.
- /// </summary>
- public class ExpiredMessageCleanupWorker : BackgroundService
- {
- private readonly ILogger<ExpiredMessageCleanupWorker> _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<ExpiredMessageCleanupWorker> 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<ModelContext>();
- 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<MessageQueue> 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");
- }
- }
|