feat(sitecallaudit): daily terminal-row purge scheduler

Add a daily purge tick to SiteCallAuditActor that drops terminal SiteCalls
rows older than the retention window via ISiteCallAuditRepository.PurgeTerminalAsync.
The threshold is computed each tick as UtcNow - RetentionDays so an operator who
lowers RetentionDays sees it on the next purge without a restart. Mirrors
AuditLogPurgeActor's daily cadence + continue-on-error posture: a purge fault is
logged and swallowed so the central singleton stays alive and retries next tick.

The purge timer is started in PreStart alongside the reconciliation timer and
gates on the same collaborators (pull client + enumerator) being available — the
repo-only test ctor injects neither, so neither background timer runs there.

Options: PurgeInterval (default 24h, clamped >= 1 min so a zero config value
can't spin the scheduler) + RetentionDays (default 365), plus a test-only
override that bypasses the clamp for millisecond cadences.

Tests (all in-memory, no live MSSQL): purge tick calls PurgeTerminalAsync with a
UtcNow - RetentionDays threshold (non-default 30 days); default retention yields
a 365-day threshold; a throwing repo does not kill the singleton (a second tick
still arrives).
This commit is contained in:
Joseph Doherty
2026-06-15 12:03:49 -04:00
parent e427b38fb3
commit e675b34500
3 changed files with 323 additions and 18 deletions
@@ -2,10 +2,12 @@ namespace ZB.MOM.WW.ScadaBridge.SiteCallAudit;
/// <summary>
/// Configuration options for the Site Call Audit (#22): stuck-call detection +
/// KPI windowing for the read-side, plus the cadence knobs for the periodic
/// per-site reconciliation pull (self-heal for lost telemetry). Mirrors the
/// KPI-relevant subset of <c>NotificationOutboxOptions</c> and the
/// scheduler-cadence shape of <c>SiteAuditReconciliationOptions</c>.
/// KPI windowing for the read-side, plus the cadence/retention knobs for the
/// two central-singleton schedulers — the periodic per-site reconciliation
/// pull (self-heal for lost telemetry) and the daily terminal-row purge.
/// Mirrors the KPI-relevant subset of <c>NotificationOutboxOptions</c> and the
/// scheduler-cadence shape of <c>SiteAuditReconciliationOptions</c> /
/// <c>AuditLogPurgeOptions</c>.
/// </summary>
public class SiteCallAuditOptions
{
@@ -92,4 +94,51 @@ public class SiteCallAuditOptions
: ReconciliationInterval < MinReconciliationInterval
? MinReconciliationInterval
: ReconciliationInterval;
// ── Purge scheduler (#22): daily terminal-row purge ──
/// <summary>
/// Period of the purge tick. Each tick drops terminal <c>SiteCalls</c> rows
/// older than the retention window via
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories.ISiteCallAuditRepository.PurgeTerminalAsync"/>.
/// Default 24 hours, matching <c>AuditLogPurgeOptions</c>. Clamped to at
/// least <see cref="MinPurgeInterval"/> via <see cref="ResolvedPurgeInterval"/>.
/// </summary>
public TimeSpan PurgeInterval { get; set; } = TimeSpan.FromHours(24);
/// <summary>
/// Test-only override for the purge tick cadence — bypasses the
/// <see cref="MinPurgeInterval"/> clamp so unit tests can drop the cadence
/// to milliseconds. Production config never sets this; leave null.
/// </summary>
public TimeSpan? PurgeIntervalOverride { get; set; }
/// <summary>
/// Retention window for terminal rows. On each purge tick a row whose
/// <c>TerminalAtUtc</c> is older than <c>UtcNow - RetentionDays</c> is
/// deleted; non-terminal rows are never purged. Default 365 days, matching
/// the central audit-store retention policy.
/// </summary>
public int RetentionDays { get; set; } = 365;
/// <summary>
/// Minimum interval the config-bound <see cref="PurgeInterval"/> can resolve
/// to. Clamps a misconfigured <c>0</c> (or negative) value away from
/// <see cref="TimeSpan.Zero"/> for the same scheduler-spin reason as
/// <see cref="MinReconciliationInterval"/>; the purge is daily so the floor
/// is a more generous 1 minute.
/// </summary>
private static readonly TimeSpan MinPurgeInterval = TimeSpan.FromMinutes(1);
/// <summary>
/// Resolves the effective purge tick interval: the test override when set
/// (bypassing the clamp), otherwise <see cref="PurgeInterval"/> clamped to at
/// least <see cref="MinPurgeInterval"/>.
/// </summary>
public TimeSpan ResolvedPurgeInterval =>
PurgeIntervalOverride is { } o
? o
: PurgeInterval < MinPurgeInterval
? MinPurgeInterval
: PurgeInterval;
}