Skip to content
This repository was archived by the owner on Jan 15, 2024. It is now read-only.

Commit 023cfdf

Browse files
Allow setting status codes to retry (#152)
For grafana/terraform-provider-grafana#893 We currently retry 429 and 5xx. This is retained as a default in this PR but there's a new `RetryStatusCodes` that, if set, overrides these statuses That way, the user that opened the issue above will be able to also retry 407 on their Azure instance **Had to do some fixes in dashboard tests, the httptest servers were not properly used, not closed and leaked out of the tests**
1 parent 655166f commit 023cfdf

File tree

4 files changed

+153
-57
lines changed

4 files changed

+153
-57
lines changed

client.go

+37-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type Config struct {
4242
NumRetries int
4343
// RetryTimeout says how long to wait before retrying a request
4444
RetryTimeout time.Duration
45+
// RetryStatusCodes contains the list of status codes to retry, use "x" as a wildcard for a single digit (default: [429, 5xx])
46+
RetryStatusCodes []string
4547
}
4648

4749
// New creates a new Grafana client.
@@ -80,6 +82,10 @@ func (c *Client) request(method, requestPath string, query url.Values, body []by
8082
err error
8183
bodyContents []byte
8284
)
85+
retryStatusCodes := c.config.RetryStatusCodes
86+
if len(retryStatusCodes) == 0 {
87+
retryStatusCodes = []string{"429", "5xx"}
88+
}
8389

8490
// retry logic
8591
for n := 0; n <= c.config.NumRetries; n++ {
@@ -115,8 +121,11 @@ func (c *Client) request(method, requestPath string, query url.Values, body []by
115121
continue
116122
}
117123

118-
// Exit the loop if we have something final to return. This is anything < 500, if it's not a 429.
119-
if resp.StatusCode < http.StatusInternalServerError && resp.StatusCode != http.StatusTooManyRequests {
124+
shouldRetry, err := matchRetryCode(resp.StatusCode, retryStatusCodes)
125+
if err != nil {
126+
return err
127+
}
128+
if !shouldRetry {
120129
break
121130
}
122131
}
@@ -179,3 +188,29 @@ func (c *Client) newRequest(method, requestPath string, query url.Values, body i
179188
req.Header.Add("Content-Type", "application/json")
180189
return req, err
181190
}
191+
192+
// matchRetryCode checks if the status code matches any of the configured retry status codes.
193+
func matchRetryCode(gottenCode int, retryCodes []string) (bool, error) {
194+
gottenCodeStr := strconv.Itoa(gottenCode)
195+
for _, retryCode := range retryCodes {
196+
if len(retryCode) != 3 {
197+
return false, fmt.Errorf("invalid retry status code: %s", retryCode)
198+
}
199+
matched := true
200+
for i := range retryCode {
201+
c := retryCode[i]
202+
if c == 'x' {
203+
continue
204+
}
205+
if gottenCodeStr[i] != c {
206+
matched = false
207+
break
208+
}
209+
}
210+
if matched {
211+
return true, nil
212+
}
213+
}
214+
215+
return false, nil
216+
}

client_test.go

+49-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"net/http"
99
"net/http/httptest"
1010
"net/url"
11+
"strconv"
12+
"strings"
1113
"testing"
1214
"time"
1315
)
@@ -209,8 +211,11 @@ func TestClient_requestWithRetries(t *testing.T) {
209211
case 2:
210212
http.Error(w, `{"error":"calm down"}`, http.StatusTooManyRequests)
211213

212-
default:
214+
case 3:
213215
w.Write([]byte(`{"foo":"bar"}`)) //nolint:errcheck
216+
217+
default:
218+
t.Errorf("unexpected retry %d", try)
214219
}
215220
}))
216221
defer ts.Close()
@@ -255,6 +260,49 @@ func TestClient_requestWithRetries(t *testing.T) {
255260
t.Logf("request successful after %d retries", try)
256261
}
257262

263+
func TestClient_CustomRetryStatusCode(t *testing.T) {
264+
body := []byte(`lorem ipsum dolor sit amet`)
265+
var try int
266+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
267+
defer r.Body.Close()
268+
269+
try++
270+
271+
switch try {
272+
case 1, 2:
273+
http.Error(w, `{"error":"weird error"}`, http.StatusUpgradeRequired)
274+
default:
275+
http.Error(w, `{"error":"failed"}`, http.StatusInternalServerError)
276+
}
277+
}))
278+
defer ts.Close()
279+
280+
httpClient := &http.Client{
281+
Transport: &customRoundTripper{},
282+
}
283+
284+
c, err := New(ts.URL, Config{
285+
NumRetries: 5,
286+
Client: httpClient,
287+
RetryTimeout: 50 * time.Millisecond,
288+
RetryStatusCodes: []string{strconv.Itoa(http.StatusUpgradeRequired)},
289+
})
290+
if err != nil {
291+
t.Fatalf("unexpected error creating client: %v", err)
292+
}
293+
294+
var got interface{}
295+
err = c.request(http.MethodPost, "/", nil, body, &got)
296+
expectedErr := "status: 500, body: {\"error\":\"failed\"}" // The 500 is not retried because it's not in RetryStatusCodes
297+
if strings.TrimSpace(err.Error()) != expectedErr {
298+
t.Fatalf("expected err: %s, got err: %v", expectedErr, err)
299+
}
300+
301+
if try != 3 {
302+
t.Fatalf("unexpected number of tries: %d", try)
303+
}
304+
}
305+
258306
type customRoundTripper struct {
259307
try int
260308
}

dashboard_test.go

+62-49
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package gapi
22

33
import (
4+
"fmt"
45
"strings"
56
"testing"
67

@@ -78,67 +79,79 @@ func TestDashboardCreateAndUpdate(t *testing.T) {
7879
}
7980

8081
func TestDashboardGet(t *testing.T) {
81-
client := gapiTestTools(t, 200, getDashboardResponse)
82+
t.Run("By slug", func(t *testing.T) {
83+
client := gapiTestTools(t, 200, getDashboardResponse)
8284

83-
resp, err := client.Dashboard("test")
84-
if err != nil {
85-
t.Error(err)
86-
}
87-
uid, ok := resp.Model["uid"]
88-
if !ok || uid != "cIBgcSjkk" {
89-
t.Errorf("Invalid uid - %s, Expected %s", uid, "cIBgcSjkk")
90-
}
91-
92-
client = gapiTestTools(t, 200, getDashboardResponse)
85+
resp, err := client.Dashboard("test")
86+
if err != nil {
87+
t.Error(err)
88+
}
89+
uid, ok := resp.Model["uid"]
90+
if !ok || uid != "cIBgcSjkk" {
91+
t.Errorf("Invalid uid - %s, Expected %s", uid, "cIBgcSjkk")
92+
}
93+
})
9394

94-
resp, err = client.DashboardByUID("cIBgcSjkk")
95-
if err != nil {
96-
t.Fatal(err)
97-
}
98-
uid, ok = resp.Model["uid"]
99-
if !ok || uid != "cIBgcSjkk" {
100-
t.Fatalf("Invalid UID - %s, Expected %s", uid, "cIBgcSjkk")
101-
}
95+
t.Run("By UID", func(t *testing.T) {
96+
client := gapiTestTools(t, 200, getDashboardResponse)
10297

103-
for _, code := range []int{401, 403, 404} {
104-
client = gapiTestTools(t, code, "error")
105-
_, err = client.Dashboard("test")
106-
if err == nil {
107-
t.Errorf("%d not detected", code)
98+
resp, err := client.DashboardByUID("cIBgcSjkk")
99+
if err != nil {
100+
t.Fatal(err)
108101
}
109-
110-
_, err = client.DashboardByUID("cIBgcSjkk")
111-
if err == nil {
112-
t.Errorf("%d not detected", code)
102+
uid, ok := resp.Model["uid"]
103+
if !ok || uid != "cIBgcSjkk" {
104+
t.Fatalf("Invalid UID - %s, Expected %s", uid, "cIBgcSjkk")
113105
}
106+
})
107+
108+
for _, code := range []int{401, 403, 404} {
109+
t.Run(fmt.Sprintf("Dashboard error: %d", code), func(t *testing.T) {
110+
client := gapiTestToolsFromCalls(t, []mockServerCall{{code, "error"}, {code, "error"}})
111+
_, err := client.Dashboard("test")
112+
if err == nil {
113+
t.Errorf("%d not detected", code)
114+
}
115+
116+
_, err = client.DashboardByUID("cIBgcSjkk")
117+
if err == nil {
118+
t.Errorf("%d not detected", code)
119+
}
120+
})
114121
}
115122
}
116123

117124
func TestDashboardDelete(t *testing.T) {
118-
client := gapiTestTools(t, 200, "")
119-
err := client.DeleteDashboard("test")
120-
if err != nil {
121-
t.Error(err)
122-
}
123-
124-
client = gapiTestTools(t, 200, "")
125-
err = client.DeleteDashboardByUID("cIBgcSjkk")
126-
if err != nil {
127-
t.Fatal(err)
128-
}
129-
130-
for _, code := range []int{401, 403, 404, 412} {
131-
client = gapiTestTools(t, code, "error")
132-
133-
err = client.DeleteDashboard("test")
134-
if err == nil {
135-
t.Errorf("%d not detected", code)
125+
t.Run("By slug", func(t *testing.T) {
126+
client := gapiTestTools(t, 200, "")
127+
err := client.DeleteDashboard("test")
128+
if err != nil {
129+
t.Error(err)
136130
}
131+
})
137132

138-
err = client.DeleteDashboardByUID("cIBgcSjkk")
139-
if err == nil {
140-
t.Errorf("%d not detected", code)
133+
t.Run("By UID", func(t *testing.T) {
134+
client := gapiTestTools(t, 200, "")
135+
err := client.DeleteDashboardByUID("cIBgcSjkk")
136+
if err != nil {
137+
t.Fatal(err)
141138
}
139+
})
140+
141+
for _, code := range []int{401, 403, 404, 412} {
142+
t.Run(fmt.Sprintf("Dashboard error: %d", code), func(t *testing.T) {
143+
client := gapiTestToolsFromCalls(t, []mockServerCall{{code, "error"}, {code, "error"}})
144+
145+
err := client.DeleteDashboard("test")
146+
if err == nil {
147+
t.Errorf("%d not detected", code)
148+
}
149+
150+
err = client.DeleteDashboardByUID("cIBgcSjkk")
151+
if err == nil {
152+
t.Errorf("%d not detected", code)
153+
}
154+
})
142155
}
143156
}
144157

mock.go mock_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@ type mockServer struct {
1919
server *httptest.Server
2020
}
2121

22-
func (m *mockServer) Close() {
23-
m.server.Close()
24-
}
25-
2622
func gapiTestTools(t *testing.T, code int, body string) *Client {
23+
t.Helper()
2724
return gapiTestToolsFromCalls(t, []mockServerCall{{code, body}})
2825
}
2926

@@ -35,6 +32,9 @@ func gapiTestToolsFromCalls(t *testing.T, calls []mockServerCall) *Client {
3532
}
3633

3734
mock.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35+
if len(mock.upcomingCalls) == 0 {
36+
t.Fatalf("unexpected call to %s %s", r.Method, r.URL)
37+
}
3838
call := mock.upcomingCalls[0]
3939
if len(calls) > 1 {
4040
mock.upcomingCalls = mock.upcomingCalls[1:]
@@ -61,7 +61,7 @@ func gapiTestToolsFromCalls(t *testing.T, calls []mockServerCall) *Client {
6161
}
6262

6363
t.Cleanup(func() {
64-
mock.Close()
64+
mock.server.Close()
6565
})
6666

6767
return client

0 commit comments

Comments
 (0)