Notify retention and cleanup
Notify separates configured policy and original incident/signing records from disposable notification mechanics. Cleanup uses the locally replicated active-tenant catalog and requires its durable checkpoint. A tenant with only old deliveries or inbox rows is still visited. An unconverged replica fails the pass; inactive tenants remain the tenant-lifecycle workflow’s responsibility.
Schedule and limits
The keeper checks once a minute. Each tenant’s five-field CleanupSchedule runs in UTC (default 0 2 * * *). The first pass catches up immediately; later successful passes wait for the next cron occurrence. Failed passes retry. AutoCleanupEnabled=false disables automatic cleanup; explicit single-tenant cleanup can run immediately.
MaxDeletesPerRun limits the total reclaimed rows across that tenant’s cleanup pass. Each state category takes at most 1,000 rows. Results and the cleanup log now include fallbackStates, digests, escalationStates and stormStates alongside the existing delivery, inbox, event and audit counts. One tenant failing does not prevent subsequent tenants from being processed.
| State | Default window / row budget | Safety boundary |
|---|---|---|
| Successful fallback attempts | Notifier:Fallback:StateRetention, 24 hours; newest 10,000 successful rows per tenant | Preserve unsuccessful attempts and recheck success plus last_attempt_at when deleting. |
| Sent digest buffers | Tenant DeliveryRetention, 90 days; newest 10,000 sent rows | Preserve collecting/sending buffers and missing or refreshed sent_at. |
| Terminal escalation state | Tenant delivery window, extended by ResolvedRetentionMultiplier when enabled; default 180 days and newest 10,000 terminal rows | Preserve active state. A trigger records terminal observation and clears it on reopen. Existing expired rows without a transition timestamp receive the migration observation time, not an invented historical time. |
| Inactive storm state | Longest of tenant StormDataRetention (7 days), configured detection windows and cooldown; newest 10,000 inactive rows | Preserve active storms and refreshed activity. Cleanup does not declare an active storm ended. |
| Correlation throttle events | Notifier:Throttler:EventRetention, 30 days | Default and requested throttle windows must be positive and fit this finite horizon. Idle keys are reclaimed too. Existing check-time cleanup can remove events outside the requested sliding window earlier. |
| Storm events | Longest of tenant storm retention, default/override detection windows and cooldown | Expire only old working events; preserve current storm state and original incident records. Existing per-key evaluation cleanup continues. |
Delivery, inbox, dead-letter and cleanup-log age policies remain in force and now share the per-pass deletion budget. Delivery deletion matches the physical partition and row identity and rechecks terminal status. Pending, queued, sending and sent deliveries remain; only delivered, failed and bounced outcomes qualify. Unread, unarchived inbox messages remain at any age. Nonpositive windows or budgets fail before reclamation.
The two additional Doctor checks are doctor.notify.retention.fallback-budget and doctor.notify.retention.mechanics-budget. They measure overdue or over-budget terminal rows under the same tenant scope. Missing terminal metadata or unmeasurable policy/catalog state is unhealthy. Retained original incident, approval and signing records are outside these mechanics budgets.
Retained records and the signing index
Source records include authored channels, rules, templates, subscriptions, calendars, maintenance/on-call configuration, fallback chains, throttle and security policy, tenant ownership/grants, original incidents and correlation decisions, regulatory reporting state, pack approvals, operator overrides, signing work and first handoff identities. Expiry may change a policy’s applicability; it does not reconstruct or erase its origin.
notify.nis2_csirt_signing_due is a derived index. The existing writer trigger keeps it current. Cleanup also calls notify.rebuild_nis2_signing_due, which reconstructs exactly the pending work with a non-null retry time while serializing with the source writer. The signed/request payloads, original fingerprints and completed/refused work remain.
The runtime DAL no longer embeds historical migration resources or exposes the unused whole-family migration registration. The four historical SQL files remain byte-identical regression inputs, embedded only by test assemblies. Production schema changes belong to StellaOps.Notify.Persistence.Consolidated. The old notify.locks declaration is not part of the current runtime; the forward migration refuses any unexpected populated copy before removal. P6 remains the selected lease implementation.
Verification
Verified against source commit f736019e17fe6a5a42826bf76901a81679c1551e. The dated verification receipt records the executed checks and table census. Re-run NotifyRetentionIntegrationTests in the Notify web test project with tools/scripts/test-targeted-xunit.ps1 -BuildProjectReferences -Restore. The persistence, consolidated-schema, Delivery and Operations suites cover their adjacent contracts. The five P13 gates are the source inventory, --tables, --owners, --owners --strict and --self-test modes of tools/scripts/validate/check-p13-retention.py --repo-root ..
Verification uses isolated PostgreSQL databases, tenant RLS, real service composition and Doctor execution. It does not claim a production rollout or a live cleanup pass.
