carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "log"
9 "net"
10 "net/http"
11 "strings"
12 "time"
13
14 "github.com/carlosframework/platform/internal/moves"
15 "github.com/carlosframework/platform/internal/registry"
16)
17
18// S1 (moves spec §7.3, codex 14; Task 14 review finding 4): the instance as
19// it runs today answers its readiness path from its own process. The probe
20// wakes a hibernating instance through the activator (ServeScheduled: the
21// same wake, no activity credit) and then asks the instance itself over its
22// own socket. Only that answer counts: anything the activator, the edge, a
23// gate or a fallback writes in its place is not the instance answering.
24
25// s1Timeout bounds one probe, wake included (§7.7's 30 s readiness window).
26const s1Timeout = 30 * time.Second
27
28// s1BodyLimit is the most of a readiness answer the probe reads; a longer
29// body fails, since it was not read in full.
30const s1BodyLimit = 1 << 20
31
32// probeInstance is S1 for rt at path. wake is the activator's entry
33// (Service.ServeScheduled in production): it wakes the instance and hands
34// the request to the handler it is given, which asks the instance over
35// rt.Socket. A wake that answers itself (a fenced or refused host, a
36// fallback) never reaches that handler. A route the activator does not
37// manage (wake returns false) is asked directly. S1 passes only on a 2xx
38// from the instance, its body read in full, within the deadline.
39func probeInstance(ctx context.Context, wake func(http.ResponseWriter, *http.Request, registry.Route, http.Handler) bool, rt registry.Route, path string) moves.Check {
40 fail := moves.Check{Reason: s1NoAnswer(rt.Host)}
41 ctx, cancel := context.WithTimeout(ctx, s1Timeout)
42 defer cancel()
43 if !strings.HasPrefix(path, "/") {
44 path = "/" + path
45 }
46 req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+rt.Host+path, nil)
47 if err != nil {
48 return fail
49 }
50 req.Host = rt.Host
51 req.RemoteAddr = "127.0.0.1:0"
52 var answered bool
53 var status int
54 var askErr error
55 ask := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56 answered = true
57 status, askErr = askInstance(r.Context(), rt.Socket, r)
58 if askErr == nil {
59 w.WriteHeader(status)
60 }
61 })
62 rec := &probeRecorder{header: http.Header{}}
63 if !wake(rec, req, rt, ask) {
64 ask.ServeHTTP(rec, req)
65 }
66 switch {
67 case !answered, askErr != nil, ctx.Err() != nil:
68 if askErr != nil {
69 log.Printf("moves: %s: S1: %v", rt.Host, askErr)
70 }
71 return fail
72 case status < 200 || status > 299:
73 return fail
74 }
75 return moves.Check{OK: true}
76}
77
78// askInstance sends r to the instance listening on socket and reads its
79// whole answer, never following a redirect.
80func askInstance(ctx context.Context, socket string, r *http.Request) (int, error) {
81 if socket == "" {
82 return 0, errors.New("the route names no socket")
83 }
84 client := &http.Client{
85 Transport: &http.Transport{
86 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
87 var d net.Dialer
88 return d.DialContext(ctx, "unix", socket)
89 },
90 DisableKeepAlives: true,
91 },
92 CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
93 }
94 // The declared target as declared: the URL keeps RawPath, so an
95 // escaped "/ready%2Fcheck" is never sent as "/ready/check".
96 u := *r.URL
97 u.Scheme, u.Host = "http", r.Host
98 out, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
99 if err != nil {
100 return 0, err
101 }
102 out.Host = r.Host
103 resp, err := client.Do(out)
104 if err != nil {
105 return 0, err
106 }
107 defer resp.Body.Close()
108 n, err := io.Copy(io.Discard, io.LimitReader(resp.Body, s1BodyLimit+1))
109 if err != nil {
110 return 0, err
111 }
112 if n > s1BodyLimit {
113 return 0, fmt.Errorf("the answer is over %d bytes", s1BodyLimit)
114 }
115 return resp.StatusCode, nil
116}
117
118// probeRecorder is the ResponseWriter the wake writes to. What it records
119// is never evidence: only askInstance's answer is.
120type probeRecorder struct {
121 header http.Header
122 status int
123}
124
125func (r *probeRecorder) Header() http.Header { return r.header }
126func (r *probeRecorder) Write(b []byte) (int, error) {
127 if r.status == 0 {
128 r.status = http.StatusOK
129 }
130 return len(b), nil
131}
132func (r *probeRecorder) WriteHeader(code int) {
133 if r.status == 0 {
134 r.status = code
135 }
136}
137