package host import ( "context" "errors" "fmt" "io" "log" "net" "net/http" "strings" "time" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/registry" ) // S1 (moves spec §7.3, codex 14; Task 14 review finding 4): the instance as // it runs today answers its readiness path from its own process. The probe // wakes a hibernating instance through the activator (ServeScheduled: the // same wake, no activity credit) and then asks the instance itself over its // own socket. Only that answer counts: anything the activator, the edge, a // gate or a fallback writes in its place is not the instance answering. // s1Timeout bounds one probe, wake included (§7.7's 30 s readiness window). const s1Timeout = 30 * time.Second // s1BodyLimit is the most of a readiness answer the probe reads; a longer // body fails, since it was not read in full. const s1BodyLimit = 1 << 20 // probeInstance is S1 for rt at path. wake is the activator's entry // (Service.ServeScheduled in production): it wakes the instance and hands // the request to the handler it is given, which asks the instance over // rt.Socket. A wake that answers itself (a fenced or refused host, a // fallback) never reaches that handler. A route the activator does not // manage (wake returns false) is asked directly. S1 passes only on a 2xx // from the instance, its body read in full, within the deadline. func probeInstance(ctx context.Context, wake func(http.ResponseWriter, *http.Request, registry.Route, http.Handler) bool, rt registry.Route, path string) moves.Check { fail := moves.Check{Reason: s1NoAnswer(rt.Host)} ctx, cancel := context.WithTimeout(ctx, s1Timeout) defer cancel() if !strings.HasPrefix(path, "/") { path = "/" + path } req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+rt.Host+path, nil) if err != nil { return fail } req.Host = rt.Host req.RemoteAddr = "127.0.0.1:0" var answered bool var status int var askErr error ask := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { answered = true status, askErr = askInstance(r.Context(), rt.Socket, r) if askErr == nil { w.WriteHeader(status) } }) rec := &probeRecorder{header: http.Header{}} if !wake(rec, req, rt, ask) { ask.ServeHTTP(rec, req) } switch { case !answered, askErr != nil, ctx.Err() != nil: if askErr != nil { log.Printf("moves: %s: S1: %v", rt.Host, askErr) } return fail case status < 200 || status > 299: return fail } return moves.Check{OK: true} } // askInstance sends r to the instance listening on socket and reads its // whole answer, never following a redirect. func askInstance(ctx context.Context, socket string, r *http.Request) (int, error) { if socket == "" { return 0, errors.New("the route names no socket") } client := &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, "unix", socket) }, DisableKeepAlives: true, }, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, } // The declared target as declared: the URL keeps RawPath, so an // escaped "/ready%2Fcheck" is never sent as "/ready/check". u := *r.URL u.Scheme, u.Host = "http", r.Host out, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return 0, err } out.Host = r.Host resp, err := client.Do(out) if err != nil { return 0, err } defer resp.Body.Close() n, err := io.Copy(io.Discard, io.LimitReader(resp.Body, s1BodyLimit+1)) if err != nil { return 0, err } if n > s1BodyLimit { return 0, fmt.Errorf("the answer is over %d bytes", s1BodyLimit) } return resp.StatusCode, nil } // probeRecorder is the ResponseWriter the wake writes to. What it records // is never evidence: only askInstance's answer is. type probeRecorder struct { header http.Header status int } func (r *probeRecorder) Header() http.Header { return r.header } func (r *probeRecorder) Write(b []byte) (int, error) { if r.status == 0 { r.status = http.StatusOK } return len(b), nil } func (r *probeRecorder) WriteHeader(code int) { if r.status == 0 { r.status = code } }