namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Supervisor; /// /// Crash-loop circuit breaker for the FOCAS Host. Matches Galaxy Tier-C defaults: /// 3 crashes within 5 minutes opens the breaker; cooldown escalates 1h → 4h → manual /// reset. A sticky alert stays live until the operator explicitly clears it so /// recurring crashes can't silently burn through the cooldown ladder overnight. /// public sealed class CircuitBreaker { public int CrashesAllowedPerWindow { get; init; } = 3; public TimeSpan Window { get; init; } = TimeSpan.FromMinutes(5); public TimeSpan[] CooldownEscalation { get; init; } = [TimeSpan.FromHours(1), TimeSpan.FromHours(4), TimeSpan.MaxValue]; private readonly List _crashesUtc = []; private DateTime? _openSinceUtc; private int _escalationLevel; public bool StickyAlertActive { get; private set; } /// /// Records a crash + returns true if the supervisor may respawn. On /// false, is how long to wait before /// trying again (TimeSpan.MaxValue means manual reset required). /// public bool TryRecordCrash(DateTime utcNow, out TimeSpan cooldownRemaining) { if (_openSinceUtc is { } openedAt) { var cooldown = CooldownEscalation[Math.Min(_escalationLevel, CooldownEscalation.Length - 1)]; if (cooldown == TimeSpan.MaxValue) { cooldownRemaining = TimeSpan.MaxValue; return false; } if (utcNow - openedAt < cooldown) { cooldownRemaining = cooldown - (utcNow - openedAt); return false; } _openSinceUtc = null; _escalationLevel++; } _crashesUtc.RemoveAll(t => utcNow - t > Window); _crashesUtc.Add(utcNow); if (_crashesUtc.Count > CrashesAllowedPerWindow) { _openSinceUtc = utcNow; StickyAlertActive = true; cooldownRemaining = CooldownEscalation[Math.Min(_escalationLevel, CooldownEscalation.Length - 1)]; return false; } cooldownRemaining = TimeSpan.Zero; return true; } public void ManualReset() { _crashesUtc.Clear(); _openSinceUtc = null; _escalationLevel = 0; StickyAlertActive = false; } }