carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package lbhealth is a box's own liveness, as the load balancer's health
2// check reads it (region HA spec 2 §6). Every fact here is this box's own:
3// a flag or a timestamp that one of its own loops sets. Nothing here reads
4// a disk, a bucket or the network, so an outage of something the boxes
5// share (the deployment bucket) never turns a working box unhealthy and
6// never gets it terminated.
7//
8// The edge answers GET or HEAD /.carlos/lb-health on port 80 from State.
9// The loops that feed it are the agent's: the claim agent sets hosting,
10// the home map latches forwarding and stamps its refresh attempts, the
11// certificate mirror latches its first load, and the edge's registry watch
12// and the activator's sweeper each beat.
13package lbhealth
14
15import (
16 "io"
17 "net/http"
18 "strings"
19 "sync"
20 "sync/atomic"
21 "time"
22)
23
24// Path is the health check's path, matched exactly.
25const Path = "/.carlos/lb-health"
26
27// HeartbeatWindow is how recent the edge's and the activator's beats must
28// be. Both loops beat every 30 seconds or faster.
29const HeartbeatWindow = 60 * time.Second
30
31// RefreshWindow is how recent the home map's last completed refresh
32// attempt must be: its 30-second interval plus its two-minute request
33// timeout, with room to spare. A failed attempt counts, so a bucket outage
34// where every attempt fails fast keeps this satisfied; a worker stuck
35// inside one attempt does not.
36const RefreshWindow = 5 * time.Minute
37
38// The condition names a 503 lists, in this order.
39const (
40 CondHosting = "hosting"
41 CondForwarding = "forwarding"
42 CondCerts = "certificates"
43 CondEdge = "edge"
44 CondActivator = "activator"
45 CondRefresh = "refresh"
46)
47
48// State is one box's liveness facts. The zero value is a box that has
49// proven nothing, and answers 503. A nil *State ignores every update.
50type State struct {
51 hosting atomic.Bool
52 mapLoaded atomic.Bool
53 keyLoaded atomic.Bool
54 certs atomic.Bool
55 edgeBeat atomic.Int64 // unix nanoseconds; 0 is never
56 refreshed atomic.Int64 // unix nanoseconds; 0 is never
57
58 mu sync.RWMutex
59 activator func() time.Time
60}
61
62// Default is the process's state. The agent runs the edge and the
63// activator in one process, so the claim wiring, the edge and the
64// activator share it.
65var Default = &State{}
66
67// SetHosting records whether the agent is in hosting mode: enrolled, with
68// its guard armed (§5). Region HA spec 2's plan 2b adds joining mode, whose
69// mode selection is what sets this false; until then the claim agent sets
70// it true once its claim wiring is running (ClaimWiring.startRegional).
71func (s *State) SetHosting(on bool) {
72 if s != nil {
73 s.hosting.Store(on)
74 }
75}
76
77// Hosting reports what SetHosting last recorded.
78func (s *State) Hosting() bool { return s != nil && s.hosting.Load() }
79
80// MapLoaded latches the first successful load of the forwarding map.
81func (s *State) MapLoaded() {
82 if s != nil {
83 s.mapLoaded.Store(true)
84 }
85}
86
87// KeyLoaded latches the first successful load of a mesh key.
88func (s *State) KeyLoaded() {
89 if s != nil {
90 s.keyLoaded.Store(true)
91 }
92}
93
94// CertsLoaded latches the first successful load of the certificate mirror
95// into the local cache the TLS handshake reads.
96func (s *State) CertsLoaded() {
97 if s != nil {
98 s.certs.Store(true)
99 }
100}
101
102// EdgeBeat records that the edge's registry watch completed a poll at t.
103func (s *State) EdgeBeat(t time.Time) {
104 if s != nil {
105 s.edgeBeat.Store(t.UnixNano())
106 }
107}
108
109// RefreshAttempted records that the home map's refresh finished an
110// attempt at t, whether it succeeded or failed.
111func (s *State) RefreshAttempted(t time.Time) {
112 if s != nil {
113 s.refreshed.Store(t.UnixNano())
114 }
115}
116
117// SetActivator gives the state the activator's own beat: the time its
118// sweeper last finished a sweep. nil means there is no activator, which
119// fails the activator condition.
120func (s *State) SetActivator(last func() time.Time) {
121 if s == nil {
122 return
123 }
124 s.mu.Lock()
125 s.activator = last
126 s.mu.Unlock()
127}
128
129func (s *State) activatorBeat() time.Time {
130 s.mu.RLock()
131 last := s.activator
132 s.mu.RUnlock()
133 if last == nil {
134 return time.Time{}
135 }
136 return last()
137}
138
139// within reports whether at is set and no older than window at now.
140func within(at, now time.Time, window time.Duration) bool {
141 return !at.IsZero() && now.Sub(at) <= window
142}
143
144func stamp(n int64) time.Time {
145 if n == 0 {
146 return time.Time{}
147 }
148 return time.Unix(0, n)
149}
150
151// Failing lists the conditions that do not hold at now, in the order of
152// the Cond names. An empty list is a healthy box. A nil *State fails
153// every condition.
154func (s *State) Failing(now time.Time) []string {
155 if s == nil {
156 return []string{CondHosting, CondForwarding, CondCerts, CondEdge, CondActivator, CondRefresh}
157 }
158 var out []string
159 if !s.hosting.Load() {
160 out = append(out, CondHosting)
161 }
162 if !s.mapLoaded.Load() || !s.keyLoaded.Load() {
163 out = append(out, CondForwarding)
164 }
165 if !s.certs.Load() {
166 out = append(out, CondCerts)
167 }
168 if !within(stamp(s.edgeBeat.Load()), now, HeartbeatWindow) {
169 out = append(out, CondEdge)
170 }
171 if !within(s.activatorBeat(), now, HeartbeatWindow) {
172 out = append(out, CondActivator)
173 }
174 if !within(stamp(s.refreshed.Load()), now, RefreshWindow) {
175 out = append(out, CondRefresh)
176 }
177 return out
178}
179
180// Serve answers the health check when r is one, and reports whether it
181// did. It matches as narrowly as the steering probe: the exact path, GET
182// or HEAD, any host. It answers 200 "ok" when every condition holds and
183// 503 naming the failing conditions otherwise, never cached. It only reads
184// s.
185func Serve(w http.ResponseWriter, r *http.Request, s *State, now time.Time) bool {
186 if r.URL.Path != Path || (r.Method != http.MethodGet && r.Method != http.MethodHead) {
187 return false
188 }
189 body, code := "ok\n", http.StatusOK
190 if failing := s.Failing(now); len(failing) > 0 {
191 body, code = "unavailable: "+strings.Join(failing, ",")+"\n", http.StatusServiceUnavailable
192 }
193 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
194 w.Header().Set("Cache-Control", "no-store")
195 w.WriteHeader(code)
196 if r.Method == http.MethodGet {
197 _, _ = io.WriteString(w, body)
198 }
199 return true
200}
201