diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SiteAuditRetentionOptions.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SiteAuditRetentionOptions.cs new file mode 100644 index 00000000..3aee06a9 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SiteAuditRetentionOptions.cs @@ -0,0 +1,60 @@ +namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site; + +/// +/// Configuration for the site-side SQLite retention purge (PLAN-04 Task 2, S1/U2). +/// The site audit store is ephemeral (~7-day retention); +/// runs +/// on a timer using these knobs. Mirrors the resolve/clamp shape of +/// SiteCallAuditOptions so a misconfigured value can never make the timer spin +/// or the retention window collapse. +/// +public class SiteAuditRetentionOptions +{ + /// Configuration section path bound in the site host. + public const string SectionName = "ScadaBridge:AuditLog:SiteRetention"; + + /// + /// Retention window in days. On each purge tick, Forwarded/Reconciled rows whose + /// OccurredAtUtc is older than UtcNow - ResolvedRetentionDays are + /// deleted (Pending rows are never purged on age alone). Default 7. Resolved value + /// is clamped to ... + /// + public int RetentionDays { get; set; } = 7; + + private const int MinRetentionDays = 1; + private const int MaxRetentionDays = 90; + + /// + /// Effective retention window: clamped to + /// [, ] so a + /// misconfigured 0 (which would purge everything) or an unbounded value can + /// never take effect. + /// + public int ResolvedRetentionDays => + RetentionDays < MinRetentionDays ? MinRetentionDays + : RetentionDays > MaxRetentionDays ? MaxRetentionDays + : RetentionDays; + + /// + /// Period of the purge tick. Default 24 hours. Resolved value is clamped to at + /// least so a zero/negative config value cannot + /// make the timer fire in a tight loop. + /// + public TimeSpan PurgeInterval { get; set; } = TimeSpan.FromHours(24); + + private static readonly TimeSpan MinPurgeInterval = TimeSpan.FromMinutes(1); + + /// + /// Effective purge period: clamped to at least + /// (the Timer/Akka zero-interval spin footgun). + /// + public TimeSpan ResolvedPurgeInterval => + PurgeInterval < MinPurgeInterval ? MinPurgeInterval : PurgeInterval; + + /// + /// Delay before the first purge tick. Default 5 minutes — short so a node that + /// restarts daily (or more often) still runs the purge shortly after start + /// rather than waiting a full it may never reach. + /// + public TimeSpan InitialDelay { get; set; } = TimeSpan.FromMinutes(5); +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsTests.cs new file mode 100644 index 00000000..da9d66da --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsTests.cs @@ -0,0 +1,33 @@ +using ZB.MOM.WW.ScadaBridge.AuditLog.Site; + +namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site; + +/// +/// Tests for clamping (PLAN-04 Task 2). +/// The resolved knobs guard the daily site retention job against misconfiguration: +/// retention days are clamped to [1, 90]; the purge interval is clamped away from +/// TimeSpan.Zero (an Akka/Timer spin footgun). +/// +public class SiteAuditRetentionOptionsTests +{ + [Theory] + [InlineData(0, 1)] // clamp floor + [InlineData(7, 7)] // default passthrough + [InlineData(365, 90)] // clamp ceiling (spec: min 1, max 90) + public void ResolvedRetentionDays_Clamps(int configured, int expected) + => Assert.Equal(expected, new SiteAuditRetentionOptions { RetentionDays = configured }.ResolvedRetentionDays); + + [Fact] + public void Defaults_Are_SevenDays_DailyPurge_FiveMinuteInitialDelay() + { + var o = new SiteAuditRetentionOptions(); + Assert.Equal(7, o.ResolvedRetentionDays); + Assert.Equal(TimeSpan.FromHours(24), o.ResolvedPurgeInterval); + Assert.Equal(TimeSpan.FromMinutes(5), o.InitialDelay); + } + + [Fact] + public void ResolvedPurgeInterval_ClampsZeroToOneMinute() + => Assert.Equal(TimeSpan.FromMinutes(1), + new SiteAuditRetentionOptions { PurgeInterval = TimeSpan.Zero }.ResolvedPurgeInterval); +}