| 1 | package edge |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "time" |
| 6 | |
| 7 | "github.com/carlosframework/platform/internal/lbhealth" |
| 8 | ) |
| 9 | |
| 10 | // The load balancer health answer (region HA spec 2 §6): GET or HEAD |
| 11 | // /.carlos/lb-health on port 80, from any host. The network load |
| 12 | // balancer's HTTP check sends the target's address as the Host header, so |
| 13 | // the steering probe, which answers only for its own hostname, cannot |
| 14 | // serve it. Every condition is this box's own; lbhealth has the list. |
| 15 | |
| 16 | // lbHealthFromEnv is the state the edge answers from: the process's |
| 17 | // (lbhealth.Default) with CARLOS_REGION_HA on, and nil with it off, which |
| 18 | // leaves the path to the ordinary :80 handling exactly as before. The |
| 19 | // activator's sweep is the hosting loop's beat; an edge with no activator |
| 20 | // (carlos edge) has none, and its answer is never 200. |
| 21 | func lbHealthFromEnv(getenv func(string) string, act Activator) *lbhealth.State { |
| 22 | if !regionHAFromEnv(getenv) { |
| 23 | return nil |
| 24 | } |
| 25 | s := lbhealth.Default |
| 26 | if a, ok := act.(interface{ LastSweep() time.Time }); ok { |
| 27 | s.SetActivator(a.LastSweep) |
| 28 | } |
| 29 | return s |
| 30 | } |
| 31 | |
| 32 | // lbHealthAnswer answers the health check when r is one and region HA is |
| 33 | // on, reporting whether it did. |
| 34 | func (e *Edge) lbHealthAnswer(w http.ResponseWriter, r *http.Request) bool { |
| 35 | if e.lbHealth == nil { |
| 36 | return false |
| 37 | } |
| 38 | return lbhealth.Serve(w, r, e.lbHealth, time.Now()) |
| 39 | } |
| 40 | |
| 41 | // servingProbeTimeout bounds servingPathLive's wait for the lock. |
| 42 | var servingProbeTimeout = time.Second |
| 43 | |
| 44 | // servingPathLive is the edge beat's proof that the :443 request path is not |
| 45 | // wedged. Watch's registry poll shares no lock with ServeHTTP (the route |
| 46 | // table is an atomic pointer, read without one), so a poll alone would keep |
| 47 | // beating over a deadlocked request path. The one lock every cacheable GET |
| 48 | // and HEAD on :443 takes is the response cache's (GetFor, maybeStore), so |
| 49 | // this takes and releases it, and answers false if that takes longer than |
| 50 | // servingProbeTimeout. A probe still waiting from an earlier tick is not |
| 51 | // stacked: the answer stays false until it lands. An edge with no cache has |
| 52 | // no such lock, and the probe passes. Nothing is added to ServeHTTP. |
| 53 | func (e *Edge) servingPathLive() bool { |
| 54 | c := e.cache |
| 55 | if c == nil { |
| 56 | return true |
| 57 | } |
| 58 | if !e.probing.CompareAndSwap(false, true) { |
| 59 | return false |
| 60 | } |
| 61 | done := make(chan struct{}) |
| 62 | go func() { |
| 63 | c.mu.Lock() |
| 64 | c.mu.Unlock() //nolint:staticcheck // take and release is the probe |
| 65 | e.probing.Store(false) |
| 66 | close(done) |
| 67 | }() |
| 68 | t := time.NewTimer(servingProbeTimeout) |
| 69 | defer t.Stop() |
| 70 | select { |
| 71 | case <-done: |
| 72 | return true |
| 73 | case <-t.C: |
| 74 | return false |
| 75 | } |
| 76 | } |
| 77 | |