feat(push): add service-owned firebase channels and delivery
Add push contracts, provider delivery and channel diagnostics. Link email channels to services and document migration and validation. Ref: IT-1074
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
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 = "PushDiagnostic",
|
||||
Timestamp = DateTimeOffset.UtcNow, ErrorMessage = outcome.Error,
|
||||
PushRegistrationId = message.Data.RegistrationId, PushBindingId = message.Data.BindingId,
|
||||
PushTokenHash = message.Data.TokenHash, InvalidPushRegistration = outcome.InvalidRegistration
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user