// refusalWatch turns D1's refusal records (release.Refusal, written by the // root pass) into one alert each and a status row each. It runs on the // agent's metrics tick. A record older than refusalAlertWindow when first // seen is not alerted, so an agent restart does not re-send old news. package host import ( "context" "log" "sync/atomic" "time" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) const refusalAlertWindow = 30 * time.Minute type refusalWatch struct{ seen map[string]time.Time } func (w *refusalWatch) tick(ctx context.Context, bakeDir string, sinks *alertSinks, now time.Time) []status.ReleaseRefusal { if w.seen == nil { w.seen = map[string]time.Time{} } recs, err := release.ListRefusals(bakeDir) if err != nil { log.Printf("agent: refusals: %v", err) return nil } // A move's held-stage refusals (D3-held) live in their own namespace // (final review finding 3) and are surfaced as they were when they // shared adoption's: a row and an alert each, seen under their own key. held, err := release.ListHeldRefusals(bakeDir) if err != nil { log.Printf("agent: held refusals: %v", err) } var rows []status.ReleaseRefusal for i, r := range append(recs, held...) { row := status.ReleaseRefusal{Host: r.Host, Version: r.Version, Reason: r.Reason, RefusedAt: r.RefusedAt, Kind: r.Kind} if !r.Recovered { row.RecoveryError = recoveryError(r) } rows = append(rows, row) key := r.Host if i >= len(recs) { key = "held/" + r.Host } if last, ok := w.seen[key]; ok && !r.RefusedAt.After(last) { continue } w.seen[key] = r.RefusedAt if now.Sub(r.RefusedAt) <= refusalAlertWindow { sinks.deliverRelease(ctx, r) } } return rows } // releaseRefusals is the metrics loop's latest answer, read by the status // loop's probe. They are separate goroutines, so this is the seam between // them (the same shape as processAlertCounters). Nil until the first tick. var releaseRefusals atomic.Pointer[[]status.ReleaseRefusal] // releaseRefusalProbe feeds status.Probes.ReleaseRefusals. func releaseRefusalProbe() []status.ReleaseRefusal { if p := releaseRefusals.Load(); p != nil { return append([]status.ReleaseRefusal(nil), (*p)...) } return nil }