diff --git a/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs b/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs index 9874968..bc17053 100644 --- a/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs +++ b/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs @@ -64,6 +64,7 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers Receive(HandleRetry); Receive(HandleDiscard); Receive(HandleKpiRequest); + Receive(HandlePerSiteKpiRequest); } /// @@ -609,6 +610,37 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers snapshot.OldestPendingAge); } + /// + /// Handles a per-site KPI request, computing the per-source-site outbox metrics with the + /// same stuck cutoff and delivered window as . + /// + private void HandlePerSiteKpiRequest(PerSiteNotificationKpiRequest request) + { + var sender = Sender; + var now = DateTimeOffset.UtcNow; + var stuckCutoff = StuckCutoff(now); + var deliveredSince = now - _options.DeliveredKpiWindow; + + ComputePerSiteKpisAsync(request.CorrelationId, stuckCutoff, deliveredSince).PipeTo( + sender, + success: response => response, + failure: ex => new PerSiteNotificationKpiResponse( + request.CorrelationId, + Success: false, + ErrorMessage: ex.GetBaseException().Message, + Sites: Array.Empty())); + } + + private async Task ComputePerSiteKpisAsync( + string correlationId, DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince) + { + using var scope = _serviceProvider.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + var sites = await repository.ComputePerSiteKpisAsync(stuckCutoff, deliveredSince); + + return new PerSiteNotificationKpiResponse(correlationId, Success: true, ErrorMessage: null, sites); + } + /// /// The instant before which a still-pending notification counts as stuck — /// offset back by . diff --git a/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorQueryTests.cs b/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorQueryTests.cs index 36da5e3..3f2a5d6 100644 --- a/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorQueryTests.cs +++ b/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorQueryTests.cs @@ -356,4 +356,46 @@ public class NotificationOutboxActorQueryTests : TestKit Assert.Equal(0, response.DeliveredLastInterval); Assert.Null(response.OldestPendingAge); } + + [Fact] + public void PerSiteKpiRequest_RepliesWithPerSiteSnapshots() + { + _repository.ComputePerSiteKpisAsync( + Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(new List + { + new("plant-a", 4, 1, 0, 9, TimeSpan.FromMinutes(7)), + }); + var actor = CreateActor(); + + actor.Tell(new PerSiteNotificationKpiRequest("corr-ps"), TestActor); + + var response = ExpectMsg(); + Assert.True(response.Success); + Assert.Null(response.ErrorMessage); + Assert.Equal("corr-ps", response.CorrelationId); + Assert.Single(response.Sites); + Assert.Equal("plant-a", response.Sites[0].SourceSiteId); + + _repository.Received(1).ComputePerSiteKpisAsync( + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public void PerSiteKpiRequest_RepositoryFault_RepliesUnsuccessful() + { + _repository.ComputePerSiteKpisAsync( + Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new InvalidOperationException("db down")); + var actor = CreateActor(); + + actor.Tell(new PerSiteNotificationKpiRequest("corr-ps"), TestActor); + + var response = ExpectMsg(); + Assert.False(response.Success); + Assert.Equal("corr-ps", response.CorrelationId); + Assert.NotNull(response.ErrorMessage); + Assert.Contains("db down", response.ErrorMessage); + Assert.Empty(response.Sites); + } }