diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/AlarmSummary.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/AlarmSummary.razor index 2619dbc4..9dadcb1d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/AlarmSummary.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/AlarmSummary.razor @@ -206,6 +206,7 @@ private bool _loading; private Timer? _refreshTimer; + private readonly ZB.MOM.WW.ScadaBridge.CentralUI.Services.PollGate _pollGate = new(); private const int _autoRefreshSeconds = 15; // ── Client-side filters ── @@ -282,10 +283,18 @@ StopTimer(); _refreshTimer = new Timer(_ => { + if (!_pollGate.TryEnter()) return; InvokeAsync(async () => { - await RefreshAsync(); - StateHasChanged(); + try + { + await RefreshAsync(); + StateHasChanged(); + } + finally + { + _pollGate.Exit(); + } }); }, null, TimeSpan.FromSeconds(_autoRefreshSeconds), TimeSpan.FromSeconds(_autoRefreshSeconds)); } diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/Health.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/Health.razor index 58b53d52..4a0d273a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/Health.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/Health.razor @@ -444,6 +444,7 @@ private IReadOnlyDictionary _siteStates = new Dictionary(); private Dictionary _siteNames = new(); private Timer? _refreshTimer; + private readonly ZB.MOM.WW.ScadaBridge.CentralUI.Services.PollGate _pollGate = new(); private int _autoRefreshSeconds = 10; // Notification Outbox headline KPIs, refreshed alongside the site states. @@ -542,10 +543,18 @@ _refreshTimer = new Timer(_ => { + if (!_pollGate.TryEnter()) return; InvokeAsync(async () => { - await RefreshNow(); - StateHasChanged(); + try + { + await RefreshNow(); + StateHasChanged(); + } + finally + { + _pollGate.Exit(); + } }); }, null, TimeSpan.FromSeconds(_autoRefreshSeconds), TimeSpan.FromSeconds(_autoRefreshSeconds)); } diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/PollGate.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/PollGate.cs new file mode 100644 index 00000000..351c6eb7 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/PollGate.cs @@ -0,0 +1,11 @@ +namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; + +/// Reentrancy guard for poll timers: a fixed-period Timer whose refresh outlives +/// the period stacks overlapping fan-outs against an already-degraded site (arch-review S3). +/// Callers TryEnter at tick start and skip the tick when the previous refresh is in flight. +public sealed class PollGate +{ + private int _inFlight; + public bool TryEnter() => Interlocked.CompareExchange(ref _inFlight, 1, 0) == 0; + public void Exit() => Volatile.Write(ref _inFlight, 0); +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/PollGateTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/PollGateTests.cs new file mode 100644 index 00000000..4ce1fb66 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/PollGateTests.cs @@ -0,0 +1,17 @@ +using ZB.MOM.WW.ScadaBridge.CentralUI.Services; +using Xunit; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests; + +public class PollGateTests +{ + [Fact] + public void TryEnter_SecondCallWhileHeld_ReturnsFalse() + { + var gate = new PollGate(); + Assert.True(gate.TryEnter()); + Assert.False(gate.TryEnter()); + gate.Exit(); + Assert.True(gate.TryEnter()); + } +}