PlotDirector/PlotLine/Services/EmailQueueWorker.cs
2026-06-08 21:50:18 +01:00

80 lines
2.6 KiB
C#

using PlotLine.Data;
namespace PlotLine.Services;
public sealed class EmailQueueWorker(
IServiceScopeFactory scopeFactory,
ILogger<EmailQueueWorker> 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<IEmailQueueRepository>();
var sender = scope.ServiceProvider.GetRequiredService<IQueuedEmailSender>();
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;
}
}