fix(email): reconcile abandoned sends
Age stale pre-delivery attempts to failed and in-delivery attempts to uncertain without provider I/O. Notify each owner with content-free guidance and keep recovery idempotent across replicas and restarts.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
namespace JobTrackerApi.Services;
|
||||
|
||||
public sealed class EmailSendAttemptRecoveryHostedService(
|
||||
IServiceScopeFactory scopes,
|
||||
IStartupReadiness startupReadiness,
|
||||
ILogger<EmailSendAttemptRecoveryHostedService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await startupReadiness.WaitUntilReadyAsync(stoppingToken);
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var scope = scopes.CreateAsyncScope();
|
||||
var result = await scope.ServiceProvider.GetRequiredService<EmailSendAttemptStore>()
|
||||
.ReconcileAbandonedAsync(stoppingToken);
|
||||
if (result.FailedPending > 0 || result.UncertainSending > 0)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Recovered abandoned email attempts: failedPending={FailedPending}, uncertainSending={UncertainSending}. No provider retry was attempted.",
|
||||
result.FailedPending, result.UncertainSending);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Email send-attempt recovery failed; provider delivery was not attempted.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user