Files
AutopilotMonitor/src/Backend
Oliver Kieselbach fce4a03d23 feat(sla): persistent SlaTenantStatus row replaces in-memory throttle, fixes 2h breach-notification spam
Background
----------
The in-memory SlaNotificationThrottleService kept its 4h cooldown in a process-
local ConcurrentDictionary. On Flex Consumption the Function host recycles
between 2h timer fires, wiping the dictionary, so the throttle was effectively
a no-op — observed in production as tenant b54dc1af receiving an SLA breach
notification every 2 hours for days on end (1 failed of 11-13 sessions, target
95%, success rate 91-92%).

Design
------
One row per tenant in a new SlaTenantStatus table (PartitionKey = tenantId,
RowKey = "status"). Flat schema with {Type}_* property prefixes for the four
breach kinds (SuccessRate, Duration, AppInstall, ConsecutiveFailures); one row,
one Upsert per evaluation cycle. Rationale documented inline: per-type rows
would be over-normalised for the access pattern (always all-types-per-tenant),
and ~50 tenants x 4 types = max 200 rows ever.

Throttle correctness
--------------------
The throttle decision (now - LastNotifiedAt >= cooldown) and the LastNotifiedAt
write happen in the same TryUpsertAsync(status, ifMatchETag) call. Two parallel
inline calls cannot both succeed in sending the same notification — the loser
gets a 412 from Table Storage, refetches, sees the updated watermark, and
either skips or schedules a new claim attempt. Up to 4 retries; on exhausted
retries we drop the notification rather than risk double-fire (at-most-once).

Cooldown is configurable: AdminConfiguration.SlaNotificationCooldownHours
(default 24, range 1-168). UI round-trip wired through the four required
files (AdminConfigContext, AdminConfigSettingsSection, types/adminConfig.ts,
SectionGlobalSettings).

Resolve path
------------
When a previously-active breach is no longer detected, the row transitions to
IsActive=false + ResolvedAt=now and a single sla_resolved notification fires.
Orphan sweep: tenants whose toggles get turned off while a breach is active
fall out of the qualifying loop, so the timer now also scans every status row
and silently clears any IsActive that no longer has a toggle backing it
(no resolved notification — the tenant disabled SLA, they don't care).

Per-type notes
--------------
- SuccessRate breach notifies with the configured warn-threshold, not the SLA
  target — a 94% rate against target=99% / threshold=95% reads "below 95%",
  not "below 99%".
- AppInstall now actually evaluates (the toggle was wired but evaluation was
  missing). Aligned to the same ISO-week window the dashboard uses
  (SlaMetricsService.GetIsoWeekKey), so notifications cannot diverge from the
  /sla page. Sample-size floor of 5 apps per week, same as the dashboard.
- AppInstall has its own notification template (own title/summary/facts plus
  TopFailingApp fact) — the builder used to silently fall through to the
  Duration template for everything not SuccessRate.
- Telemetry Period tag derived from breachType: CurrentMonth for SuccessRate
  and Duration, CurrentWeek for AppInstall — so AppInsights / ops dashboards
  don't group AppInstall under the wrong period.
- AppInstall ops events now carry the actual installer totals (10 evaluated,
  4 failed) instead of 0/0.
- ConsecutiveFailures: inline (ingest) path detects breaches, timer path
  detects resolve. Both paths write to the same row through the same CAS
  retry loop.

Clock injection
---------------
SlaBreachEvaluationService takes an optional Func<DateTime> nowProvider
(default () => DateTime.UtcNow) so the week-window regression test runs
deterministically on every test day instead of silently skipping when today
happens to fall in the first ISO week of the month.

Removed
-------
- SlaNotificationThrottleService and its tests. The in-memory model is gone;
  there is no scenario where it would be wanted alongside the persistent row.

Tests
-----
- 21 new lifecycle + mapping + telemetry tests (CAS conflict + retry, CAS
  exhausted-retries drops notification, threshold-not-target in notification,
  orphan clearing, AppInstall template / week-window / totals, ConsecutiveFailures
  inline persistence, multi-type race-probe, period-mapping theory).
- Backend xUnit: 1218 passed.
- Web vitest: 266 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:47:58 +02:00
..
2026-03-26 07:02:33 +01:00