diff --git a/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs b/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs index 430ed37..947a2cc 100644 --- a/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs +++ b/src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs @@ -59,6 +59,8 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers Receive(_ => HandleDispatchTick()); Receive(_ => _dispatching = false); Receive(_ => HandlePurgeTick()); + // No-op: purge has no in-flight guard to lower, and the outcome is already logged + // by the PipeTo projections, so PurgeComplete carries nothing to act on. Receive(_ => { }); Receive(HandleQuery); Receive(HandleStatusQuery); @@ -297,9 +299,11 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers /// /// Handles a purge tick by launching an asynchronous sweep that bulk-deletes terminal /// notification rows older than . - /// Purges are daily and idempotent, so no in-flight guard is needed; the outcome is piped - /// back to only so a faulted purge is logged on the actor thread and - /// never wedges anything. + /// Purges are daily and idempotent, so no in-flight guard is needed. + /// self-isolates its faults — it logs internally and never faults its task — so the + /// success projection is the normal completion path that logs the deleted count. The + /// failure projection is kept as a belt-and-braces backup, consistent with + /// /. /// private void HandlePurgeTick() { @@ -324,13 +328,26 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers /// /// Runs a single purge sweep: resolves a scoped /// and bulk-deletes terminal rows created before , returning the - /// deleted count. + /// deleted count. The whole body is wrapped in a try/catch so the returned task never + /// faults — scope creation, service resolution, and the bulk delete can all throw, and + /// self-isolating the fault here keeps the fault-handling strategy symmetric with + /// . On failure the exception is logged and 0 is returned. /// private async Task RunPurgePass(DateTimeOffset cutoff) { - using var scope = _serviceProvider.CreateScope(); - var repository = scope.ServiceProvider.GetRequiredService(); - return await repository.DeleteTerminalOlderThanAsync(cutoff); + try + { + using var scope = _serviceProvider.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + return await repository.DeleteTerminalOlderThanAsync(cutoff); + } + catch (Exception ex) + { + // Scope/service resolution or the bulk delete faulted; swallow and log so the + // returned task completes normally, mirroring RunDispatchPass. + _logger.LogError(ex, "Purge sweep failed unexpectedly."); + return 0; + } } ///