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);
+ }
+}