diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index bc2a9294f4..1b3fd083fd 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -3180,6 +3180,76 @@ ownership is `internal/alerts/alert_identity_migration_test.go`, `internal/monitoring/monitor_alert_override_migration_test.go`, and `frontend-modern/src/features/alerts/__tests__/alertsConfigurationModel.snapshot.test.ts`. +### Alert config snapshots are owned copies + +`Manager.GetConfig()` returns `AlertConfig.Clone()`, a typed deep copy that +shares no map, slice or pointer with the live config (custom rule filter +values are copied as decoded JSON values). Callers encode, persist +and edit that snapshot outside `m.mu`: the GET config handler, alert +activation, the identity migration, backup evaluation and diagnostics. It used +to be a shallow copy, so `SaveAlertConfig` normalizing it in place, or +`UpdateConfig` re-normalizing the overrides of a re-applied snapshot, wrote the +live maps while evaluation or another encoder read them, and Go aborted Pulse +with a concurrent map write. `SaveAlertConfig` also clones before normalizing, +because a config just handed to `UpdateConfig` is the live config. Persistence +keeps an explicit `0` type-level delay as no delay on save and load, matching +`NormalizeTimeThresholds`. It used to store and load it as the 5-second factory +delay, which the noisy-gauge rule stretches to 300 seconds for memory and +temperature, and the shared map write was what made the running manager agree. +Pulse saves never wrote a `0`, so only a newly sent or hand-edited `0` changes +meaning. `internal/alerts/config/clone_test.go` +fills every field and fails when a new reference field is not copied. +Regression ownership is `internal/alerts/resolved_lock_discipline_test.go` and +`internal/config/persistence_alert_ownership_test.go`. + +### Alert evaluation owns the thresholds it normalizes + +Host, guest and Docker container evaluation copy what they read from +`m.config` while holding `m.mu`, and only then fill in defaults. +`CheckHost` and `CheckGuest` clone a per-disk +override's `Disk` threshold before `ensureHysteresisThreshold` sets a missing +`Clear` to the trigger minus 5. They used to normalize the live override's +pointer after unlocking, so a `GetConfig()` clone under the read lock raced +the write, and the saved override changed to whatever the last poll filled in. +The live override now keeps the clear it was saved with, and the config API +returns that value. Docker container evaluation resolves its thresholds from +cloned `DockerDefaults` plus the override under the same lock, and reads the +restart-loop and memory-limit settings under the lock. It used to point into +`m.config.DockerDefaults` with no lock while `UpdateConfig` replaced +`m.config`. The test-only `getThresholdForMetricFromConfig`, which normalized +in place, is gone. `internal/alerts/alerts_test.go` +`TestAlertEvaluationLeavesTheLiveConfigToTheLock` runs host, guest and Docker +evaluation against `GetConfig()` and `UpdateConfig(GetConfig())` under +`-race`. + +### Config-save reevaluation judges a filesystem alert by its own threshold + +`UpdateConfig` re-judges every active alert, and a filesystem usage alert's +threshold resolves through the same per-filesystem helper its evaluator uses. +`hostDiskUsageThresholdNoLock` resolves an agent filesystem +(`agent:/disk: