X Tutup
Skip to content

Commit fac2575

Browse files
Add retries to Codespaces API client (cli#5064)
1 parent 47a6aff commit fac2575

File tree

2 files changed

+102
-29
lines changed

2 files changed

+102
-29
lines changed

internal/codespaces/api/api.go

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type API struct {
5757
vscsAPI string
5858
githubAPI string
5959
githubServer string
60+
retryBackoff time.Duration
6061
}
6162

6263
type httpClient interface {
@@ -79,6 +80,7 @@ func New(serverURL, apiURL, vscsURL string, httpClient httpClient) *API {
7980
vscsAPI: strings.TrimSuffix(vscsURL, "/"),
8081
githubAPI: strings.TrimSuffix(apiURL, "/"),
8182
githubServer: strings.TrimSuffix(serverURL, "/"),
83+
retryBackoff: 100 * time.Millisecond,
8284
}
8385
}
8486

@@ -301,24 +303,24 @@ func findNextPage(linkValue string) string {
301303
// If the codespace is not found, an error is returned.
302304
// If includeConnection is true, it will return the connection information for the codespace.
303305
func (a *API) GetCodespace(ctx context.Context, codespaceName string, includeConnection bool) (*Codespace, error) {
304-
req, err := http.NewRequest(
305-
http.MethodGet,
306-
a.githubAPI+"/user/codespaces/"+codespaceName,
307-
nil,
308-
)
309-
if err != nil {
310-
return nil, fmt.Errorf("error creating request: %w", err)
311-
}
312-
313-
if includeConnection {
314-
q := req.URL.Query()
315-
q.Add("internal", "true")
316-
q.Add("refresh", "true")
317-
req.URL.RawQuery = q.Encode()
318-
}
319-
320-
a.setHeaders(req)
321-
resp, err := a.do(ctx, req, "/user/codespaces/*")
306+
resp, err := a.withRetry(func() (*http.Response, error) {
307+
req, err := http.NewRequest(
308+
http.MethodGet,
309+
a.githubAPI+"/user/codespaces/"+codespaceName,
310+
nil,
311+
)
312+
if err != nil {
313+
return nil, fmt.Errorf("error creating request: %w", err)
314+
}
315+
if includeConnection {
316+
q := req.URL.Query()
317+
q.Add("internal", "true")
318+
q.Add("refresh", "true")
319+
req.URL.RawQuery = q.Encode()
320+
}
321+
a.setHeaders(req)
322+
return a.do(ctx, req, "/user/codespaces/*")
323+
})
322324
if err != nil {
323325
return nil, fmt.Errorf("error making request: %w", err)
324326
}
@@ -344,17 +346,18 @@ func (a *API) GetCodespace(ctx context.Context, codespaceName string, includeCon
344346
// StartCodespace starts a codespace for the user.
345347
// If the codespace is already running, the returned error from the API is ignored.
346348
func (a *API) StartCodespace(ctx context.Context, codespaceName string) error {
347-
req, err := http.NewRequest(
348-
http.MethodPost,
349-
a.githubAPI+"/user/codespaces/"+codespaceName+"/start",
350-
nil,
351-
)
352-
if err != nil {
353-
return fmt.Errorf("error creating request: %w", err)
354-
}
355-
356-
a.setHeaders(req)
357-
resp, err := a.do(ctx, req, "/user/codespaces/*/start")
349+
resp, err := a.withRetry(func() (*http.Response, error) {
350+
req, err := http.NewRequest(
351+
http.MethodPost,
352+
a.githubAPI+"/user/codespaces/"+codespaceName+"/start",
353+
nil,
354+
)
355+
if err != nil {
356+
return nil, fmt.Errorf("error creating request: %w", err)
357+
}
358+
a.setHeaders(req)
359+
return a.do(ctx, req, "/user/codespaces/*/start")
360+
})
358361
if err != nil {
359362
return fmt.Errorf("error making request: %w", err)
360363
}
@@ -686,3 +689,19 @@ func (a *API) do(ctx context.Context, req *http.Request, spanName string) (*http
686689
func (a *API) setHeaders(req *http.Request) {
687690
req.Header.Set("Accept", "application/vnd.github.v3+json")
688691
}
692+
693+
// withRetry takes a generic function that sends an http request and retries
694+
// only when the returned response has a >=500 status code.
695+
func (a *API) withRetry(f func() (*http.Response, error)) (resp *http.Response, err error) {
696+
for i := 0; i < 5; i++ {
697+
resp, err = f()
698+
if err != nil {
699+
return nil, err
700+
}
701+
if resp.StatusCode < 500 {
702+
break
703+
}
704+
time.Sleep(a.retryBackoff * (time.Duration(i) + 1))
705+
}
706+
return resp, err
707+
}

internal/codespaces/api/api_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,57 @@ func TestListCodespaces_unlimited(t *testing.T) {
114114
t.Fatalf("expected codespace-249, got %s", codespaces[0].Name)
115115
}
116116
}
117+
118+
func TestRetries(t *testing.T) {
119+
var callCount int
120+
csName := "test_codespace"
121+
handler := func(w http.ResponseWriter, r *http.Request) {
122+
if callCount == 3 {
123+
err := json.NewEncoder(w).Encode(Codespace{
124+
Name: csName,
125+
})
126+
if err != nil {
127+
t.Fatal(err)
128+
}
129+
return
130+
}
131+
callCount++
132+
w.WriteHeader(502)
133+
}
134+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler(w, r) }))
135+
t.Cleanup(srv.Close)
136+
a := &API{
137+
githubAPI: srv.URL,
138+
client: &http.Client{},
139+
}
140+
cs, err := a.GetCodespace(context.Background(), "test", false)
141+
if err != nil {
142+
t.Fatal(err)
143+
}
144+
if callCount != 3 {
145+
t.Fatalf("expected at least 2 retries but got %d", callCount)
146+
}
147+
if cs.Name != csName {
148+
t.Fatalf("expected codespace name to be %q but got %q", csName, cs.Name)
149+
}
150+
callCount = 0
151+
handler = func(w http.ResponseWriter, r *http.Request) {
152+
callCount++
153+
err := json.NewEncoder(w).Encode(Codespace{
154+
Name: csName,
155+
})
156+
if err != nil {
157+
t.Fatal(err)
158+
}
159+
}
160+
cs, err = a.GetCodespace(context.Background(), "test", false)
161+
if err != nil {
162+
t.Fatal(err)
163+
}
164+
if callCount != 1 {
165+
t.Fatalf("expected no retries but got %d calls", callCount)
166+
}
167+
if cs.Name != csName {
168+
t.Fatalf("expected codespace name to be %q but got %q", csName, cs.Name)
169+
}
170+
}

0 commit comments

Comments
 (0)
X Tutup