v2.31.0-dev8 ec8c5b9

readiness.go RawHistoryFolder

1package container
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"net"
8	"net/http"
9	"time"
10
11	"github.com/bnema/zerowrap"
12)
13
14var errProbeTimeout = errors.New("probe timeout")
15
16func probeLoop(
17	ctx context.Context,
18	deadline time.Time,
19	baseSleep time.Duration,
20	defaultAttemptTimeout time.Duration,
21	work func(attemptCtx context.Context) (success bool, err error),
22) error {
23	for {
24		if err := ctx.Err(); err != nil {
25			return err
26		}
27		if time.Now().After(deadline) {
28			return errProbeTimeout
29		}
30
31		remaining := time.Until(deadline)
32		attemptTimeout := defaultAttemptTimeout
33		if remaining < attemptTimeout {
34			attemptTimeout = remaining
35		}
36
37		attemptCtx, cancel := context.WithTimeout(ctx, attemptTimeout)
38		success, err := work(attemptCtx)
39		cancel()
40		if err != nil {
41			return err
42		}
43		if success {
44			return nil
45		}
46
47		sleepInterval := baseSleep
48		if remProbe := time.Until(deadline); remProbe < sleepInterval {
49			sleepInterval = remProbe
50		}
51		if ctxDeadline, ok := ctx.Deadline(); ok {
52			if remCtx := time.Until(ctxDeadline); remCtx < sleepInterval {
53				sleepInterval = remCtx
54			}
55		}
56		if sleepInterval <= 0 {
57			if ctx.Err() != nil {
58				return ctx.Err()
59			}
60			return errProbeTimeout
61		}
62
63		t := time.NewTimer(sleepInterval)
64		select {
65		case <-t.C:
66		case <-ctx.Done():
67			t.Stop()
68			return ctx.Err()
69		}
70	}
71}
72
73// tcpProbe attempts a TCP connection to addr, retrying every 500ms until
74// success or timeout. This is the universal fallback readiness check —
75// it verifies the process is at least accepting connections.
76func tcpProbe(ctx context.Context, addr string, timeout time.Duration) error {
77	log := zerowrap.FromCtx(ctx)
78	deadline := time.Now().Add(timeout)
79	dialer := &net.Dialer{}
80	attempts := 0
81	var lastErr error
82	err := probeLoop(ctx, deadline, 500*time.Millisecond, time.Second, func(attemptCtx context.Context) (bool, error) {
83		conn, err := dialer.DialContext(attemptCtx, "tcp", addr)
84		attempts++
85		if err == nil {
86			conn.Close()
87			log.Debug().Str("addr", addr).Int("attempts", attempts).Msg("TCP probe connected")
88			return true, nil
89		}
90
91		lastErr = err
92		if attempts <= 3 || attempts%10 == 0 {
93			log.Debug().Err(err).Str("addr", addr).Int("attempt", attempts).Msg("TCP probe attempt failed")
94		}
95		return false, nil
96	})
97	if errors.Is(err, errProbeTimeout) {
98		return fmt.Errorf("TCP probe timeout after %s: %s not reachable (attempts=%d, last_error=%v)", timeout, addr, attempts, lastErr)
99	}
100	return err
101}
102
103// httpProbe performs HTTP GET requests to url, retrying every 1s until a
104// 2xx/3xx response or timeout. Used when gordon.health label is set.
105func httpProbe(ctx context.Context, url string, timeout time.Duration) error {
106	client := &http.Client{
107		CheckRedirect: func(req *http.Request, via []*http.Request) error {
108			// Don't follow redirects — treat 3xx as a successful response.
109			return http.ErrUseLastResponse
110		},
111	}
112	deadline := time.Now().Add(timeout)
113	var lastStatus int
114	err := probeLoop(ctx, deadline, time.Second, 2*time.Second, func(attemptCtx context.Context) (bool, error) {
115		req, reqErr := http.NewRequestWithContext(attemptCtx, http.MethodGet, url, nil)
116		if reqErr != nil {
117			return false, reqErr
118		}
119
120		resp, err := client.Do(req)
121		if err != nil {
122			return false, nil
123		}
124
125		lastStatus = resp.StatusCode
126		resp.Body.Close()
127		if lastStatus >= 200 && lastStatus < 400 {
128			return true, nil
129		}
130
131		return false, nil
132	})
133	if errors.Is(err, errProbeTimeout) {
134		return fmt.Errorf("HTTP probe timeout after %s: last status %d from %s", timeout, lastStatus, url)
135	}
136	return err
137}
138
139// httpAliveProbe performs HTTP GET requests to url, retrying every 1s until
140// any HTTP response is received or timeout. Unlike httpProbe, ANY status code
141// counts as success -- the goal is to verify the HTTP server is accepting and
142// processing requests, not that a specific endpoint is healthy.
143// Used as the default readiness check for web containers.
144func httpAliveProbe(ctx context.Context, url string, timeout time.Duration) error {
145	client := &http.Client{
146		CheckRedirect: func(req *http.Request, via []*http.Request) error {
147			return http.ErrUseLastResponse
148		},
149	}
150	deadline := time.Now().Add(timeout)
151	err := probeLoop(ctx, deadline, time.Second, 2*time.Second, func(attemptCtx context.Context) (bool, error) {
152		req, reqErr := http.NewRequestWithContext(attemptCtx, http.MethodGet, url, nil)
153		if reqErr != nil {
154			return false, reqErr
155		}
156
157		resp, err := client.Do(req)
158		if err != nil {
159			// Connection refused, timeout, etc. -- not ready yet.
160			return false, nil
161		}
162		resp.Body.Close()
163		// Any HTTP response means the server is alive and processing.
164		return true, nil
165	})
166	if errors.Is(err, errProbeTimeout) {
167		return fmt.Errorf("HTTP alive probe timeout after %s: no HTTP response from %s", timeout, url)
168	}
169	return err
170}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close