Skip to content

Commit d2ad9ec

Browse files
authored
Re-use http.Request (#42)
* constants for pester methods, http package constants for standart methods and status codes, fix typos in sample * Reuse request, don/'t create new for retries, just copy body. Simplify logical flow. PostForm = Post. * fix typo * module support * simplify GET/HEAD handling, return PostForm for logging, format code * fix request creation bug
1 parent a71a0c1 commit d2ad9ec

File tree

6 files changed

+97
-74
lines changed

6 files changed

+97
-74
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

benchmarks/go.mod

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module pester/benchmarks
2+
3+
require (
4+
github.com/sethgrid/pester v1.0.0
5+
)
6+
7+
replace github.com/sethgrid/pester v1.0.0 => ../
8+
9+
go 1.14

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/sethgrid/pester
2+
3+
go 1.14

pester.go

+70-69
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,26 @@ import (
1212
"math/rand"
1313
"net/http"
1414
"net/url"
15+
"strings"
1516
"sync"
1617
"time"
1718
)
1819

1920
const (
20-
methodDo = "Do"
21-
methodGet = "Get"
22-
methodHead = "Head"
23-
methodPost = "Post"
24-
methodPostForm = "PostForm"
21+
methodDo = "Do"
22+
methodGet = "Get"
23+
methodHead = "Head"
24+
methodPost = "Post"
25+
methodPostForm = "PostForm"
26+
headerKeyContentType = "Content-Type"
27+
contentTypeFormURLEncoded = "application/x-www-form-urlencoded"
2528
)
2629

2730
//ErrUnexpectedMethod occurs when an http.Client method is unable to be mapped from a calling method in the pester client
2831
var ErrUnexpectedMethod = errors.New("unexpected client method, must be one of Do, Get, Head, Post, or PostFrom")
2932

3033
// ErrReadingBody happens when we cannot read the body bytes
34+
// Deprecated: use ErrReadingRequestBody
3135
var ErrReadingBody = errors.New("error reading body")
3236

3337
// ErrReadingRequestBody happens when we cannot read the request body bytes
@@ -91,7 +95,7 @@ type params struct {
9195
req *http.Request
9296
url string
9397
bodyType string
94-
body io.Reader
98+
body io.ReadCloser
9599
data url.Values
96100
}
97101

@@ -184,6 +188,16 @@ func (c *Client) Wait() {
184188
c.wg.Wait()
185189
}
186190

191+
func (c *Client) copyBody(src io.ReadCloser) ([]byte, error) {
192+
b, err := ioutil.ReadAll(src)
193+
if err != nil {
194+
return nil, ErrReadingRequestBody
195+
}
196+
src.Close()
197+
198+
return b, nil
199+
}
200+
187201
// pester provides all the logic of retries, concurrency, backoff, and logging
188202
func (c *Client) pester(p params) (*http.Response, error) {
189203
resultCh := make(chan result)
@@ -227,95 +241,80 @@ func (c *Client) pester(p params) (*http.Response, error) {
227241
}
228242

229243
// if we have a request body, we need to save it for later
230-
var originalRequestBody []byte
231-
var originalBody []byte
232-
var err error
233-
if p.req != nil && p.req.Body != nil {
234-
originalRequestBody, err = ioutil.ReadAll(p.req.Body)
235-
if err != nil {
236-
return nil, ErrReadingRequestBody
237-
}
238-
p.req.Body.Close()
244+
var (
245+
request *http.Request
246+
originalBody []byte
247+
err error
248+
)
249+
250+
if p.req != nil && p.req.Body != nil && p.body == nil {
251+
originalBody, err = c.copyBody(p.req.Body)
252+
} else if p.body != nil {
253+
originalBody, err = c.copyBody(p.body)
239254
}
240-
if p.body != nil {
241-
originalBody, err = ioutil.ReadAll(p.body)
242-
if err != nil {
243-
return nil, ErrReadingBody
244-
}
255+
256+
switch p.method {
257+
case methodDo:
258+
request = p.req
259+
case methodGet, methodHead:
260+
request, err = http.NewRequest(p.verb, p.url, nil)
261+
case methodPostForm, methodPost:
262+
request, err = http.NewRequest(http.MethodPost, p.url, ioutil.NopCloser(bytes.NewBuffer(originalBody)))
263+
default:
264+
err = ErrUnexpectedMethod
265+
}
266+
if err != nil {
267+
return nil, err
268+
}
269+
270+
if len(p.bodyType) > 0 {
271+
request.Header.Set(headerKeyContentType, p.bodyType)
245272
}
246273

247274
AttemptLimit := c.MaxRetries
248275
if AttemptLimit <= 0 {
249276
AttemptLimit = 1
250277
}
251278

252-
for req := 0; req < concurrency; req++ {
279+
for n := 0; n < concurrency; n++ {
253280
c.wg.Add(1)
254281
totalSentRequests.Add(1)
255-
go func(n int, p params) {
282+
go func(n int, req *http.Request) {
256283
defer c.wg.Done()
257284
defer totalSentRequests.Done()
258285

259-
var err error
260286
for i := 1; i <= AttemptLimit; i++ {
261287
c.wg.Add(1)
262288
defer c.wg.Done()
289+
263290
select {
264291
case <-finishCh:
265292
return
266293
default:
267294
}
268295

269-
// rehydrate the body (it is drained each read)
270-
if len(originalRequestBody) > 0 {
271-
p.req.Body = ioutil.NopCloser(bytes.NewBuffer(originalRequestBody))
272-
}
273-
if len(originalBody) > 0 {
274-
p.body = bytes.NewBuffer(originalBody)
275-
}
276-
277-
var resp *http.Response
278-
// route the calls
279-
switch p.method {
280-
case methodDo:
281-
resp, err = httpClient.Do(p.req)
282-
case methodGet:
283-
resp, err = httpClient.Get(p.url)
284-
case methodHead:
285-
resp, err = httpClient.Head(p.url)
286-
case methodPost:
287-
resp, err = httpClient.Post(p.url, p.bodyType, p.body)
288-
case methodPostForm:
289-
resp, err = httpClient.PostForm(p.url, p.data)
290-
default:
291-
err = ErrUnexpectedMethod
292-
}
293-
296+
resp, err := httpClient.Do(req)
294297
// Early return if we have a valid result
295298
// Only retry (ie, continue the loop) on 5xx status codes and 429
296-
297299
if err == nil && resp.StatusCode < http.StatusInternalServerError && (resp.StatusCode != http.StatusTooManyRequests || (resp.StatusCode == http.StatusTooManyRequests && !c.RetryOnHTTP429)) {
298300
multiplexCh <- result{resp: resp, err: err, req: n, retry: i}
299301
return
300302
}
301303

302-
loggingContext := context.Background()
303-
if p.req != nil {
304-
loggingContext = p.req.Context()
305-
}
306-
304+
loggingContext := req.Context()
307305
c.log(
308306
loggingContext,
309307
ErrEntry{
310308
Time: time.Now(),
311309
Method: p.method,
312-
Verb: p.verb,
313-
URL: p.url,
310+
Verb: req.Method,
311+
URL: req.URL.String(),
314312
Request: n,
315313
Retry: i + 1, // would remove, but would break backward compatibility
316314
Attempt: i,
317315
Err: err,
318-
})
316+
},
317+
)
319318

320319
// if it is the last iteration, grab the result (which is an error at this point)
321320
if i == AttemptLimit {
@@ -324,14 +323,11 @@ func (c *Client) pester(p params) (*http.Response, error) {
324323
}
325324

326325
//If the request has been cancelled, skip retries
327-
if p.req != nil {
328-
ctx := p.req.Context()
329-
select {
330-
case <-ctx.Done():
331-
multiplexCh <- result{resp: resp, err: ctx.Err()}
332-
return
333-
default:
334-
}
326+
select {
327+
case <-req.Context().Done():
328+
multiplexCh <- result{resp: resp, err: req.Context().Err()}
329+
return
330+
default:
335331
}
336332

337333
// if we are retrying, we should close this response body to free the fd
@@ -342,7 +338,12 @@ func (c *Client) pester(p params) (*http.Response, error) {
342338
// prevent a 0 from causing the tick to block, pass additional microsecond
343339
<-time.After(c.Backoff(i) + 1*time.Microsecond)
344340
}
345-
}(req, p)
341+
}(n, request)
342+
343+
// rehydrate the body (it is drained each read)
344+
if request.Body != nil {
345+
request.Body = ioutil.NopCloser(bytes.NewBuffer(originalBody))
346+
}
346347
}
347348

348349
// spin off the go routine so it can continually listen in on late results and close the response bodies
@@ -373,8 +374,8 @@ func (c *Client) pester(p params) (*http.Response, error) {
373374
defer c.Unlock()
374375
c.SuccessReqNum = res.req
375376
c.SuccessRetryNum = res.retry
376-
return res.resp, res.err
377377

378+
return res.resp, res.err
378379
}
379380

380381
// LogString provides a string representation of the errors the client has seen
@@ -440,12 +441,12 @@ func (c *Client) Head(url string) (resp *http.Response, err error) {
440441

441442
// Post provides the same functionality as http.Client.Post
442443
func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
443-
return c.pester(params{method: methodPost, url: url, bodyType: bodyType, body: body, verb: http.MethodPost})
444+
return c.pester(params{method: methodPost, url: url, bodyType: bodyType, body: ioutil.NopCloser(body), verb: http.MethodPost})
444445
}
445446

446447
// PostForm provides the same functionality as http.Client.PostForm
447448
func (c *Client) PostForm(url string, data url.Values) (resp *http.Response, err error) {
448-
return c.pester(params{method: methodPostForm, url: url, data: data, verb: http.MethodPost})
449+
return c.pester(params{method: methodPostForm, url: url, bodyType: contentTypeFormURLEncoded, body: ioutil.NopCloser(strings.NewReader(data.Encode())), verb: http.MethodPost})
449450
}
450451

451452
// set RetryOnHTTP429 for clients,

sample/go.mod

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module pester/sample
2+
3+
require (
4+
github.com/sethgrid/pester v1.0.0
5+
)
6+
7+
replace github.com/sethgrid/pester v1.0.0 => ../
8+
9+
go 1.14

sample/main.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,15 @@ func randoHandler(w http.ResponseWriter, r *http.Request) {
164164
var code int
165165
switch rand.Intn(10) {
166166
case 0:
167-
code = 404
167+
code = http.StatusNotFound
168168
case 1:
169-
code = 400
169+
code = http.StatusBadRequest
170170
case 2:
171-
code = 501
171+
code = http.StatusNotImplemented
172172
case 3:
173-
code = 500
173+
code = http.StatusInternalServerError
174174
default:
175-
code = 200
175+
code = http.StatusOK
176176
}
177177

178178
log.Printf("incoming request on :9000 - will return %d in %d ms", code, delay)

0 commit comments

Comments
 (0)