-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathretry_test.go
More file actions
139 lines (127 loc) · 4.47 KB
/
Copy pathretry_test.go
File metadata and controls
139 lines (127 loc) · 4.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Package dinahosting tests for the retry logic in doRequest. These tests do
// NOT require live credentials: they spin up an httptest server that lets
// each test case shape the sequence of responses returned to the provider.
package dinahosting
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"sync/atomic"
"testing"
"time"
)
// newTestProvider returns a Provider wired up with a short initial backoff
// via a custom HTTPClient so tests do not wait seconds between retries.
func newTestProvider() *Provider {
return &Provider{
Username: "u",
Password: "p",
HTTPClient: &http.Client{
Timeout: 5 * time.Second,
},
}
}
// serverURL returns the URL for an httptest server as a *url.URL so it can
// be fed directly to doRequest.
func serverURL(t *testing.T, s *httptest.Server) *url.URL {
t.Helper()
u, err := url.Parse(s.URL)
if err != nil {
t.Fatalf("url.Parse(%q): %v", s.URL, err)
}
return u
}
// TestDoRequest_RetriesOn5xx verifies that doRequest retries when the API
// responds with a 5xx status and eventually succeeds.
func TestDoRequest_RetriesOn5xx(t *testing.T) {
var attempts int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n := atomic.AddInt32(&attempts, 1)
if n < 3 {
w.WriteHeader(http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"responseCode":1000,"message":"Success.","data":[]}`)
}))
defer srv.Close()
p := newTestProvider()
got, err := p.doRequest(context.Background(), serverURL(t, srv))
if err != nil {
t.Fatalf("doRequest() unexpected error: %v", err)
}
if !got.ok() {
t.Errorf("doRequest() response not OK: %+v", got)
}
if n := atomic.LoadInt32(&attempts); n != 3 {
t.Errorf("expected 3 attempts, got %d", n)
}
}
// TestDoRequest_RetriesOnTransportError verifies that doRequest retries when
// the transport layer fails (e.g. connection refused) and gives up after
// exhausting all retries.
func TestDoRequest_RetriesOnTransportError(t *testing.T) {
// Start a server and close it immediately so any connection attempt
// fails with "connection refused".
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
u := serverURL(t, srv)
srv.Close()
p := newTestProvider()
start := time.Now()
_, err := p.doRequest(context.Background(), u)
elapsed := time.Since(start)
if err == nil {
t.Fatal("doRequest() expected error when server is unreachable")
}
// With maxRetries=3 and initial backoff 1s we expect at least 1+2+4=7s
// of backoff before giving up.
if elapsed < 6*time.Second {
t.Errorf("doRequest() returned in %v; expected retries to make it take longer", elapsed)
}
}
// TestDoRequest_NoRetryOnBusinessError ensures we do NOT retry when the API
// responded with HTTP 200 but a non-success `responseCode` in the body:
// those errors are deterministic and retrying would only amplify load.
func TestDoRequest_NoRetryOnBusinessError(t *testing.T) {
var attempts int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&attempts, 1)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"responseCode":2302,"errors":[{"code":2302,"message":"already exists"}]}`)
}))
defer srv.Close()
p := newTestProvider()
got, err := p.doRequest(context.Background(), serverURL(t, srv))
if err != nil {
t.Fatalf("doRequest() unexpected error: %v", err)
}
if got.ok() {
t.Errorf("doRequest() expected non-OK response; got: %+v", got)
}
if n := atomic.LoadInt32(&attempts); n != 1 {
t.Errorf("expected exactly 1 attempt for business-logic error, got %d", n)
}
}
// TestDoRequest_ContextCancelled verifies that doRequest returns promptly
// when the caller's context is cancelled during the backoff between
// attempts instead of waiting out the full backoff schedule.
func TestDoRequest_ContextCancelled(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
p := newTestProvider()
start := time.Now()
_, err := p.doRequest(ctx, serverURL(t, srv))
elapsed := time.Since(start)
if err == nil {
t.Fatal("doRequest() expected error when context is cancelled")
}
if elapsed > 2*time.Second {
t.Errorf("doRequest() took %v after context cancellation; expected it to return quickly", elapsed)
}
}