feat(notification-outbox): route NotificationSubmit to the outbox actor

This commit is contained in:
Joseph Doherty
2026-05-19 02:38:04 -04:00
parent b88c75c116
commit 2ff62a2ceb
2 changed files with 143 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Messages.Communication;
using ScadaLink.Commons.Messages.Health;
using ScadaLink.Commons.Messages.Notification;
using ScadaLink.HealthMonitoring;
namespace ScadaLink.Communication.Actors;
@@ -66,6 +67,15 @@ public class CentralCommunicationActor : ReceiveActor
private ICancelable? _refreshSchedule;
/// <summary>
/// Proxy <see cref="IActorRef"/> for the central NotificationOutboxActor cluster singleton.
/// Set via <see cref="RegisterNotificationOutbox"/> — the Host creates the singleton proxy
/// after this actor and registers it (mirrors how the site-side actor receives its
/// runtime <see cref="IActorRef"/>s). Null until registration completes; a notification
/// arriving before then is rejected with a non-accepted ack so the site retries.
/// </summary>
private IActorRef? _notificationOutboxProxy;
/// <summary>
/// DistributedPubSub topic used to fan health reports out to the peer
/// central node so both per-node aggregators stay in sync. See
@@ -105,6 +115,61 @@ public class CentralCommunicationActor : ReceiveActor
// Route enveloped messages to sites
Receive<SiteEnvelope>(HandleSiteEnvelope);
// Notification Outbox: the Host registers the outbox singleton proxy after this
// actor is created (the proxy cannot exist before this actor's construction).
Receive<RegisterNotificationOutbox>(msg =>
{
_notificationOutboxProxy = msg.OutboxProxy;
_log.Info("Registered notification outbox proxy");
});
// Notification Outbox ingest: a site forwards a buffered NotificationSubmit to the
// central cluster via ClusterClient. Forward to the outbox proxy so the original
// Sender (the site's ClusterClient path) is preserved and the NotificationSubmitAck
// routes straight back to the site.
Receive<NotificationSubmit>(HandleNotificationSubmit);
// Notification Outbox status query: forward to the outbox proxy, preserving Sender
// so the NotificationStatusResponse routes back to the querying site.
Receive<NotificationStatusQuery>(HandleNotificationStatusQuery);
}
private void HandleNotificationSubmit(NotificationSubmit msg)
{
if (_notificationOutboxProxy == null)
{
// No outbox proxy registered yet. A non-accepted ack makes the site's
// Store-and-Forward forwarder treat this as transient and retry later.
_log.Warning(
"Cannot route NotificationSubmit {0} — notification outbox not available",
msg.NotificationId);
Sender.Tell(new NotificationSubmitAck(
msg.NotificationId, Accepted: false, Error: "notification outbox not available"));
return;
}
_log.Debug("Routing NotificationSubmit {0} to the notification outbox", msg.NotificationId);
_notificationOutboxProxy.Forward(msg);
}
private void HandleNotificationStatusQuery(NotificationStatusQuery msg)
{
if (_notificationOutboxProxy == null)
{
// No outbox proxy registered yet. Reply Found: false so the querying site
// falls back to its local Store-and-Forward buffer to resolve the status.
_log.Warning(
"Cannot route NotificationStatusQuery {0} — notification outbox not available",
msg.NotificationId);
Sender.Tell(new NotificationStatusResponse(
msg.CorrelationId, Found: false, Status: "Unknown",
RetryCount: 0, LastError: null, DeliveredAt: null));
return;
}
_log.Debug("Routing NotificationStatusQuery {0} to the notification outbox", msg.NotificationId);
_notificationOutboxProxy.Forward(msg);
}
private void HandleHeartbeat(HeartbeatMessage heartbeat)
@@ -391,3 +456,11 @@ internal record SiteAddressCacheLoaded(Dictionary<string, List<string>> SiteCont
/// due to site disconnection (WP-5).
/// </summary>
public record DebugStreamTerminated(string SiteId, string CorrelationId);
/// <summary>
/// Registers the central NotificationOutboxActor singleton proxy with the
/// <see cref="CentralCommunicationActor"/> so site-forwarded <see cref="NotificationSubmit"/>
/// and <see cref="NotificationStatusQuery"/> messages can be routed to it. Sent by the Host
/// after the outbox singleton proxy is created.
/// </summary>
public record RegisterNotificationOutbox(IActorRef OutboxProxy);