using Akka.Actor; using Akka.Event; using Microsoft.Extensions.Logging; using ZB.MOM.WW.ScadaBridge.HealthMonitoring; namespace ZB.MOM.WW.ScadaBridge.Host.Actors; /// /// Subscribes to Akka.NET dead letter events, logs them, and tracks count /// for health monitoring integration. /// /// Warning logging is rate-limited to per /// : a failover/undeploy storm can produce thousands /// of dead letters, and one Warning per letter is an unbounded log flood /// (review 01 [Low]). Once the per-window cap is hit, per-letter warnings are /// suppressed and a single summary of the suppressed count is logged when the /// window rolls over. The health-metric count is never throttled — every /// dead letter is counted. /// public class DeadLetterMonitorActor : ReceiveActor, IWithTimers { /// Maximum per-letter Warning logs emitted within one . public const int MaxWarningsPerWindow = 10; /// Rolling window over which applies. internal static readonly TimeSpan WarningWindow = TimeSpan.FromMinutes(1); private long _deadLetterCount; private int _warningsThisWindow; private long _suppressedThisWindow; private readonly ISiteHealthCollector? _healthCollector; /// Akka timer scheduler (assigned by the runtime via ). public ITimerScheduler Timers { get; set; } = null!; /// /// Initializes the actor and registers dead-letter message handlers. /// /// Logger for dead-letter events. /// Optional health collector used to increment the dead-letter metric. public DeadLetterMonitorActor(ILogger logger, ISiteHealthCollector? healthCollector = null) { _healthCollector = healthCollector; Receive(dl => { // Metric counting is never throttled. _deadLetterCount++; _healthCollector?.IncrementDeadLetter(); if (_warningsThisWindow < MaxWarningsPerWindow) { _warningsThisWindow++; logger.LogWarning( "Dead letter: {MessageType} from {Sender} to {Recipient}", dl.Message.GetType().Name, dl.Sender, dl.Recipient); if (_warningsThisWindow == MaxWarningsPerWindow) logger.LogWarning( "Dead-letter warning limit ({Limit}) reached — suppressing per-letter warnings for {Window}", MaxWarningsPerWindow, WarningWindow); } else { _suppressedThisWindow++; } }); Receive(_ => { if (_suppressedThisWindow > 0) logger.LogWarning( "Suppressed {Count} dead-letter warnings in the last {Window}", _suppressedThisWindow, WarningWindow); _warningsThisWindow = 0; _suppressedThisWindow = 0; }); Receive(_ => Sender.Tell(new DeadLetterCountResponse(_deadLetterCount))); } /// protected override void PreStart() { Context.System.EventStream.Subscribe(Self, typeof(DeadLetter)); Timers.StartPeriodicTimer("dl-window", WindowTick.Instance, WarningWindow); } /// protected override void PostStop() { Context.System.EventStream.Unsubscribe(Self, typeof(DeadLetter)); } /// Periodic self-message that rolls the warning-rate-limit window. private sealed class WindowTick { public static readonly WindowTick Instance = new(); private WindowTick() { } } } /// /// Message to request the current dead letter count. /// public sealed class GetDeadLetterCount { public static readonly GetDeadLetterCount Instance = new(); private GetDeadLetterCount() { } } /// /// Response containing the current dead letter count. /// public sealed record DeadLetterCountResponse(long Count);