119 lines
4.3 KiB
C#
119 lines
4.3 KiB
C#
using Akka.Actor;
|
|
using Akka.Event;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Host.Actors;
|
|
|
|
/// <summary>
|
|
/// Subscribes to Akka.NET dead letter events, logs them, and tracks count
|
|
/// for health monitoring integration.
|
|
///
|
|
/// Warning logging is rate-limited to <see cref="MaxWarningsPerWindow"/> per
|
|
/// <see cref="WarningWindow"/>: 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 <b>never</b> throttled — every
|
|
/// dead letter is counted.
|
|
/// </summary>
|
|
public class DeadLetterMonitorActor : ReceiveActor, IWithTimers
|
|
{
|
|
/// <summary>Maximum per-letter Warning logs emitted within one <see cref="WarningWindow"/>.</summary>
|
|
public const int MaxWarningsPerWindow = 10;
|
|
|
|
/// <summary>Rolling window over which <see cref="MaxWarningsPerWindow"/> applies.</summary>
|
|
internal static readonly TimeSpan WarningWindow = TimeSpan.FromMinutes(1);
|
|
|
|
private long _deadLetterCount;
|
|
private int _warningsThisWindow;
|
|
private long _suppressedThisWindow;
|
|
private readonly ISiteHealthCollector? _healthCollector;
|
|
|
|
/// <summary>Akka timer scheduler (assigned by the runtime via <see cref="IWithTimers"/>).</summary>
|
|
public ITimerScheduler Timers { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Initializes the actor and registers dead-letter message handlers.
|
|
/// </summary>
|
|
/// <param name="logger">Logger for dead-letter events.</param>
|
|
/// <param name="healthCollector">Optional health collector used to increment the dead-letter metric.</param>
|
|
public DeadLetterMonitorActor(ILogger<DeadLetterMonitorActor> logger, ISiteHealthCollector? healthCollector = null)
|
|
{
|
|
_healthCollector = healthCollector;
|
|
|
|
Receive<DeadLetter>(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<WindowTick>(_ =>
|
|
{
|
|
if (_suppressedThisWindow > 0)
|
|
logger.LogWarning(
|
|
"Suppressed {Count} dead-letter warnings in the last {Window}",
|
|
_suppressedThisWindow, WarningWindow);
|
|
|
|
_warningsThisWindow = 0;
|
|
_suppressedThisWindow = 0;
|
|
});
|
|
|
|
Receive<GetDeadLetterCount>(_ => Sender.Tell(new DeadLetterCountResponse(_deadLetterCount)));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void PreStart()
|
|
{
|
|
Context.System.EventStream.Subscribe(Self, typeof(DeadLetter));
|
|
Timers.StartPeriodicTimer("dl-window", WindowTick.Instance, WarningWindow);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void PostStop()
|
|
{
|
|
Context.System.EventStream.Unsubscribe(Self, typeof(DeadLetter));
|
|
}
|
|
|
|
/// <summary>Periodic self-message that rolls the warning-rate-limit window.</summary>
|
|
private sealed class WindowTick
|
|
{
|
|
public static readonly WindowTick Instance = new();
|
|
private WindowTick() { }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Message to request the current dead letter count.
|
|
/// </summary>
|
|
public sealed class GetDeadLetterCount
|
|
{
|
|
public static readonly GetDeadLetterCount Instance = new();
|
|
private GetDeadLetterCount() { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response containing the current dead letter count.
|
|
/// </summary>
|
|
public sealed record DeadLetterCountResponse(long Count);
|