aba4173827
Share template keys across email and push with independent editors and revisions. Render localized actions and preserve version-one delivery. Ref: IT-1627
72 lines
3.6 KiB
C#
72 lines
3.6 KiB
C#
namespace HrynCo.NotificationService.Worker.Services.Push;
|
|
|
|
using HrynCo.NotificationService.Contracts.Messages;
|
|
using HrynCo.NotificationService.DAL.Abstract.Push;
|
|
using Hrynco.RabbitMq;
|
|
|
|
public sealed class PushDeliveryService(IPushDeliveryRepository deliveries, IPushSender sender, IRabbitMqPublisher publisher)
|
|
{
|
|
public async Task ProcessAsync(Guid messageId, SendPushMessage message, CancellationToken cancellationToken)
|
|
{
|
|
PushDeliveryOutcome outcome = await DeliverWithRetryAsync(messageId, message.Data, cancellationToken);
|
|
await PublishResultAsync(message, outcome, cancellationToken);
|
|
}
|
|
|
|
private async Task<PushDeliveryOutcome> DeliverWithRetryAsync(Guid messageId, SendPushMessageData data,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
for (int attempt = 0; attempt < 3; attempt++)
|
|
{
|
|
try
|
|
{
|
|
return await deliveries.DeliverOnceAsync(messageId, data.ServiceName,
|
|
ct => SendAsync(messageId, data, ct), cancellationToken);
|
|
}
|
|
catch (PushRetryException exception)
|
|
{
|
|
var delay = RetryDelay(attempt, exception.RetryAfter);
|
|
if (attempt == 2 || DateTimeOffset.UtcNow.Add(delay) >= data.ExpiresAt) break;
|
|
PushDeliveryMetrics.RecordRetry();
|
|
// The failed transaction has been disposed; do not hold a DB lock while backing off.
|
|
await Task.Delay(delay, cancellationToken);
|
|
}
|
|
}
|
|
return await deliveries.DeliverOnceAsync(messageId, data.ServiceName,
|
|
_ => Task.FromResult(new PushDeliveryOutcome("Push delivery retries exhausted or message expired.")), cancellationToken);
|
|
}
|
|
|
|
private async Task<PushDeliveryOutcome> SendAsync(Guid messageId, SendPushMessageData data, CancellationToken cancellationToken)
|
|
{
|
|
var outcome = await sender.SendAsync(messageId, data, cancellationToken);
|
|
PushDeliveryMetrics.RecordOutcome(outcome.Error is null);
|
|
return outcome;
|
|
}
|
|
|
|
internal static TimeSpan RetryDelay(int attempt, TimeSpan retryAfter) =>
|
|
TimeSpan.FromSeconds(Math.Max(60 * Math.Pow(2, attempt), retryAfter.TotalSeconds) + Random.Shared.NextDouble() * 5);
|
|
|
|
public async Task CompleteFailureAsync(Guid messageId, SendPushMessage message, CancellationToken cancellationToken)
|
|
{
|
|
var outcome = await deliveries.DeliverOnceAsync(messageId, message.Data.ServiceName,
|
|
_ => Task.FromResult(new PushDeliveryOutcome("Push processing retries exhausted.")), cancellationToken);
|
|
await PublishResultAsync(message, outcome, cancellationToken);
|
|
}
|
|
|
|
public Task PublishResultAsync(SendPushMessage message, PushDeliveryOutcome outcome, CancellationToken cancellationToken)
|
|
{
|
|
var replyTo = message.CorrelationContext.ReplyTo;
|
|
if (string.IsNullOrWhiteSpace(replyTo)) return Task.CompletedTask;
|
|
return publisher.PublishAsync(replyTo, new NotificationResultMessage
|
|
{
|
|
CorrelationContext = message.CorrelationContext with { ReplyTo = null },
|
|
Data = new NotificationResultData
|
|
{
|
|
ServiceName = message.Data.ServiceName, RecipientEmail = "", TemplateKey = message.Data.Version == 1 ? "PushDiagnostic" : message.Data.TemplateKey!,
|
|
Timestamp = DateTimeOffset.UtcNow, ErrorMessage = outcome.Error,
|
|
PushRegistrationId = message.Data.RegistrationId, PushBindingId = message.Data.BindingId,
|
|
PushTokenHash = message.Data.TokenHash, InvalidPushRegistration = outcome.InvalidRegistration
|
|
}
|
|
}, cancellationToken);
|
|
}
|
|
}
|