From b58d810dbeabf46f13d9159e69e660c248ca22fb Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 9 Jul 2026 09:26:42 -0400 Subject: [PATCH] fix(purge): short first tick on all three purge timers so daily-recycled nodes still purge --- .../Central/AuditLogPurgeActor.cs | 6 ++- .../Types/PurgeTimerSchedule.cs | 33 +++++++++++++++ .../KpiHistoryRecorderActor.cs | 10 +++-- .../SiteCallAuditActor.cs | 5 ++- .../Types/PurgeTimerScheduleTests.cs | 40 +++++++++++++++++++ 5 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 src/ZB.MOM.WW.ScadaBridge.Commons/Types/PurgeTimerSchedule.cs create mode 100644 tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/PurgeTimerScheduleTests.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeActor.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeActor.cs index ed1ea3d2..dc95d731 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeActor.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; +using ZB.MOM.WW.ScadaBridge.Commons.Types; namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central; @@ -93,7 +94,10 @@ public class AuditLogPurgeActor : ReceiveActor base.PreStart(); var interval = _purgeOptions.Interval; _timer = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable( - initialDelay: interval, + // Short first tick (min(interval, 5min)): purges are idempotent, and using the + // full interval as the initial delay meant a daily-recycled node never purged + // (arch-review 04, S9). Fast test cadences (interval < 5min) are unchanged. + initialDelay: PurgeTimerSchedule.InitialDelay(interval), interval: interval, receiver: Self, message: PurgeTick.Instance, diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/PurgeTimerSchedule.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/PurgeTimerSchedule.cs new file mode 100644 index 00000000..2c3bcdf4 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/PurgeTimerSchedule.cs @@ -0,0 +1,33 @@ +namespace ZB.MOM.WW.ScadaBridge.Commons.Types; + +/// +/// Shared initial-delay rule for the central retention-purge singletons +/// (Audit Log partition purge, Site Call Audit terminal purge, KPI History +/// sample purge). All three run on a long (up to 24 h) interval; scheduling the +/// FIRST tick after a full interval meant a node that recycles more often than +/// the interval — e.g. a daily container restart against a 24 h cadence — would +/// never actually purge (arch-review 04, S9). +/// +/// +/// Purges are idempotent, so an early first sweep is always safe. Taking +/// min(interval, ) guarantees the first purge +/// runs within five minutes of startup while leaving the steady-state cadence — +/// and the millisecond intervals the actor test suites use — unchanged. +/// +public static class PurgeTimerSchedule +{ + /// + /// Upper bound on the delay before the first purge tick. Short enough that a + /// frequently-recycled node still purges, long enough not to add startup churn. + /// + public static readonly TimeSpan ShortFirstTick = TimeSpan.FromMinutes(5); + + /// + /// The initial delay a purge timer should use for its first tick given its + /// steady-state : the interval itself when it is + /// already shorter than (preserving fast test + /// cadences), otherwise capped at . + /// + public static TimeSpan InitialDelay(TimeSpan interval) => + interval < ShortFirstTick ? interval : ShortFirstTick; +} diff --git a/src/ZB.MOM.WW.ScadaBridge.KpiHistory/KpiHistoryRecorderActor.cs b/src/ZB.MOM.WW.ScadaBridge.KpiHistory/KpiHistoryRecorderActor.cs index 9a632c23..18a887e9 100644 --- a/src/ZB.MOM.WW.ScadaBridge.KpiHistory/KpiHistoryRecorderActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.KpiHistory/KpiHistoryRecorderActor.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; +using ZB.MOM.WW.ScadaBridge.Commons.Types; namespace ZB.MOM.WW.ScadaBridge.KpiHistory; @@ -119,12 +120,15 @@ public class KpiHistoryRecorderActor : ReceiveActor, IWithTimers initialDelay: TimeSpan.FromSeconds(5), interval: _options.SampleInterval); - // The purge is daily and idempotent — no initial fast tick; the first sweep - // fires after a full PurgeInterval. + // The purge is idempotent, so it takes a short first tick (min(PurgeInterval, 5min)): + // scheduling the first sweep a full PurgeInterval out meant a node recycled more often + // than the interval (e.g. daily restart vs. daily cadence) would never purge + // (arch-review 04, S9). Fast test cadences (interval < 5min) are unchanged. Timers.StartPeriodicTimer( PurgeTimerKey, PurgeTick.Instance, - _options.PurgeInterval); + initialDelay: PurgeTimerSchedule.InitialDelay(_options.PurgeInterval), + interval: _options.PurgeInterval); } /// diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs index 1e64c856..b4fde689 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs @@ -415,7 +415,10 @@ public class SiteCallAuditActor : ReceiveActor var interval = _options.ResolvedPurgeInterval; _purgeTimer = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable( - initialDelay: interval, + // Short first tick (min(interval, 5min)): purges are idempotent, and a full-interval + // initial delay meant a daily-recycled node never purged (arch-review 04, S9). Fast + // test cadences (interval < 5min) are unchanged. + initialDelay: PurgeTimerSchedule.InitialDelay(interval), interval: interval, receiver: Self, message: PurgeTick.Instance, diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/PurgeTimerScheduleTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/PurgeTimerScheduleTests.cs new file mode 100644 index 00000000..5591e10c --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/PurgeTimerScheduleTests.cs @@ -0,0 +1,40 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types; + +/// +/// Covers the shared purge-timer initial-delay rule (arch-review 04, S9): the +/// three central purge singletons must NOT wait a full (up to 24 h) interval +/// before their first tick, or a node recycled daily would never purge. The +/// initial delay is min(interval, 5 min). +/// +public class PurgeTimerScheduleTests +{ + [Fact] + public void InitialDelay_LongInterval_IsCappedAtShortFirstTick() + { + // A daily (24 h) cadence must still fire its first purge within 5 minutes. + Assert.Equal(PurgeTimerSchedule.ShortFirstTick, PurgeTimerSchedule.InitialDelay(TimeSpan.FromHours(24))); + } + + [Fact] + public void InitialDelay_IntervalShorterThanShortFirstTick_IsUnchanged() + { + // A millisecond test cadence must be preserved verbatim (min picks the interval), + // so the existing fast-tick actor suites keep their sub-second timing. + var fast = TimeSpan.FromMilliseconds(100); + Assert.Equal(fast, PurgeTimerSchedule.InitialDelay(fast)); + } + + [Fact] + public void InitialDelay_IntervalEqualToShortFirstTick_IsThatValue() + { + Assert.Equal(PurgeTimerSchedule.ShortFirstTick, PurgeTimerSchedule.InitialDelay(PurgeTimerSchedule.ShortFirstTick)); + } + + [Fact] + public void ShortFirstTick_IsFiveMinutes() + { + Assert.Equal(TimeSpan.FromMinutes(5), PurgeTimerSchedule.ShortFirstTick); + } +}