using PlotLine.Data; namespace PlotLine.Services; public sealed class EmailQueueWorker( IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService { private const int BatchSize = 10; private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(10); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation("Email queue worker started."); await ProcessQueueAsync(stoppingToken); using var timer = new PeriodicTimer(PollInterval); while (await timer.WaitForNextTickAsync(stoppingToken)) { await ProcessQueueAsync(stoppingToken); } } private async Task ProcessQueueAsync(CancellationToken stoppingToken) { try { using var scope = scopeFactory.CreateScope(); var queue = scope.ServiceProvider.GetRequiredService(); var sender = scope.ServiceProvider.GetRequiredService(); await queue.ResetStuckSendingAsync(); var emails = await queue.DequeueBatchAsync(BatchSize); if (!emails.Any()) { return; } logger.LogInformation("Email queue worker dequeued {EmailCount} email(s).", emails.Count); foreach (var email in emails) { if (stoppingToken.IsCancellationRequested) { return; } try { await sender.SendAsync(email, stoppingToken); await queue.MarkSentAsync(email.EmailQueueID); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { return; } catch (Exception ex) { logger.LogWarning(ex, "Queued email {EmailQueueID} failed on attempt {AttemptNumber}.", email.EmailQueueID, email.AttemptCount + 1); await queue.MarkFailedAsync(email.EmailQueueID, BuildSafeError(ex)); } } } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { } catch (Exception ex) { logger.LogError(ex, "Email queue worker cycle failed."); } } private static string BuildSafeError(Exception exception) { var message = exception.Message; return message.Length > 2000 ? message[..2000] : message; } }