carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// refusalWatch turns D1's refusal records (release.Refusal, written by the
2// root pass) into one alert each and a status row each. It runs on the
3// agent's metrics tick. A record older than refusalAlertWindow when first
4// seen is not alerted, so an agent restart does not re-send old news.
5package host
6
7import (
8 "context"
9 "log"
10 "sync/atomic"
11 "time"
12
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/status"
15)
16
17const refusalAlertWindow = 30 * time.Minute
18
19type refusalWatch struct{ seen map[string]time.Time }
20
21func (w *refusalWatch) tick(ctx context.Context, bakeDir string, sinks *alertSinks, now time.Time) []status.ReleaseRefusal {
22 if w.seen == nil {
23 w.seen = map[string]time.Time{}
24 }
25 recs, err := release.ListRefusals(bakeDir)
26 if err != nil {
27 log.Printf("agent: refusals: %v", err)
28 return nil
29 }
30 // A move's held-stage refusals (D3-held) live in their own namespace
31 // (final review finding 3) and are surfaced as they were when they
32 // shared adoption's: a row and an alert each, seen under their own key.
33 held, err := release.ListHeldRefusals(bakeDir)
34 if err != nil {
35 log.Printf("agent: held refusals: %v", err)
36 }
37 var rows []status.ReleaseRefusal
38 for i, r := range append(recs, held...) {
39 row := status.ReleaseRefusal{Host: r.Host, Version: r.Version, Reason: r.Reason, RefusedAt: r.RefusedAt, Kind: r.Kind}
40 if !r.Recovered {
41 row.RecoveryError = recoveryError(r)
42 }
43 rows = append(rows, row)
44 key := r.Host
45 if i >= len(recs) {
46 key = "held/" + r.Host
47 }
48 if last, ok := w.seen[key]; ok && !r.RefusedAt.After(last) {
49 continue
50 }
51 w.seen[key] = r.RefusedAt
52 if now.Sub(r.RefusedAt) <= refusalAlertWindow {
53 sinks.deliverRelease(ctx, r)
54 }
55 }
56 return rows
57}
58
59// releaseRefusals is the metrics loop's latest answer, read by the status
60// loop's probe. They are separate goroutines, so this is the seam between
61// them (the same shape as processAlertCounters). Nil until the first tick.
62var releaseRefusals atomic.Pointer[[]status.ReleaseRefusal]
63
64// releaseRefusalProbe feeds status.Probes.ReleaseRefusals.
65func releaseRefusalProbe() []status.ReleaseRefusal {
66 if p := releaseRefusals.Load(); p != nil {
67 return append([]status.ReleaseRefusal(nil), (*p)...)
68 }
69 return nil
70}
71