Files
hrynco-notification-service/HrynCo.NotificationService.Worker.Services/Push/PushDeliveryService.cs
T
agrynco 1631786ce2 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
2026-10-24 12:31:35 +03:00

72 lines
3.5 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 = "PushDiagnostic",
Timestamp = DateTimeOffset.UtcNow, ErrorMessage = outcome.Error,
PushRegistrationId = message.Data.RegistrationId, PushBindingId = message.Data.BindingId,
PushTokenHash = message.Data.TokenHash, InvalidPushRegistration = outcome.InvalidRegistration
}
}, cancellationToken);
}
}