Skip to content

Commit 371d153

Browse files
committed
Add WithRetryUntil request opt which can be used with any CSAPI call
This will repeat a request until the condition is satisfied. Intended use: #415
1 parent 4418d2c commit 371d153

File tree

1 file changed

+65
-12
lines changed

1 file changed

+65
-12
lines changed

internal/client/client.go

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package client
22

33
import (
44
"bytes"
5+
"context"
56
"crypto/hmac"
67
"crypto/sha1" // nolint:gosec
78
"encoding/hex"
89
"encoding/json"
910
"fmt"
11+
"io"
1012
"io/ioutil"
1113
"net/http"
1214
"net/http/httputil"
@@ -27,6 +29,17 @@ const (
2729
SharedSecret = "complement"
2830
)
2931

32+
type CtxKey string
33+
34+
const (
35+
CtxKeyWithRetryUntil CtxKey = "complement_retry_until" // contains *retryUntilParams
36+
)
37+
38+
type retryUntilParams struct {
39+
timeout time.Duration
40+
untilFn func(*http.Response) bool
41+
}
42+
3043
// RequestOpt is a functional option which will modify an outgoing HTTP request.
3144
// See functions starting with `With...` in this package for more info.
3245
type RequestOpt func(req *http.Request)
@@ -441,6 +454,16 @@ func WithQueries(q url.Values) RequestOpt {
441454
}
442455
}
443456

457+
// WithRetryUntil will retry the request until the provided function returns true. Times out after
458+
// `timeout`, which will then fail the test.
459+
func WithRetryUntil(timeout time.Duration, untilFn func(res *http.Response) bool) RequestOpt {
460+
return func(req *http.Request) {
461+
until := req.Context().Value(CtxKeyWithRetryUntil).(*retryUntilParams)
462+
until.timeout = timeout
463+
until.untilFn = untilFn
464+
}
465+
}
466+
444467
// MustDoFunc is the same as DoFunc but fails the test if the returned HTTP response code is not 2xx.
445468
func (c *CSAPI) MustDoFunc(t *testing.T, method string, paths []string, opts ...RequestOpt) *http.Response {
446469
t.Helper()
@@ -479,6 +502,9 @@ func (c *CSAPI) DoFunc(t *testing.T, method string, paths []string, opts ...Requ
479502
if c.AccessToken != "" {
480503
req.Header.Set("Authorization", "Bearer "+c.AccessToken)
481504
}
505+
retryUntil := &retryUntilParams{}
506+
ctx := context.WithValue(req.Context(), CtxKeyWithRetryUntil, retryUntil)
507+
req = req.WithContext(ctx)
482508

483509
// set functional options
484510
for _, o := range opts {
@@ -502,21 +528,48 @@ func (c *CSAPI) DoFunc(t *testing.T, method string, paths []string, opts ...Requ
502528
t.Logf("Request body: <binary:%s>", contentType)
503529
}
504530
}
505-
// Perform the HTTP request
506-
res, err := c.Client.Do(req)
507-
if err != nil {
508-
t.Fatalf("CSAPI.DoFunc response returned error: %s", err)
509-
}
510-
// debug log the response
511-
if c.Debug && res != nil {
512-
var dump []byte
513-
dump, err = httputil.DumpResponse(res, true)
531+
now := time.Now()
532+
for {
533+
// Perform the HTTP request
534+
res, err := c.Client.Do(req)
514535
if err != nil {
515-
t.Fatalf("CSAPI.DoFunc failed to dump response body: %s", err)
536+
t.Fatalf("CSAPI.DoFunc response returned error: %s", err)
537+
}
538+
// debug log the response
539+
if c.Debug && res != nil {
540+
var dump []byte
541+
dump, err = httputil.DumpResponse(res, true)
542+
if err != nil {
543+
t.Fatalf("CSAPI.DoFunc failed to dump response body: %s", err)
544+
}
545+
t.Logf("%s", string(dump))
546+
}
547+
if retryUntil == nil || retryUntil.timeout == 0 {
548+
return res // don't retry
549+
}
550+
551+
// check the condition, make a copy of the response body first in case the check consumes it
552+
var resBody []byte
553+
if res.Body != nil {
554+
resBody, err = ioutil.ReadAll(res.Body)
555+
if err != nil {
556+
t.Fatalf("CSAPI.DoFunc failed to read response body for RetryUntil check: %s", err)
557+
}
558+
res.Body = io.NopCloser(bytes.NewBuffer(resBody))
516559
}
517-
t.Logf("%s", string(dump))
560+
if retryUntil.untilFn(res) {
561+
// remake the response and return
562+
res.Body = io.NopCloser(bytes.NewBuffer(resBody))
563+
return res
564+
}
565+
// condition not satisfied, do we timeout yet?
566+
if time.Since(now) > retryUntil.timeout {
567+
t.Fatalf("CSAPI.DoFunc RetryUntil: %v %v timed out after %v", method, req.URL, retryUntil.timeout)
568+
}
569+
t.Logf("CSAPI.DoFunc RetryUntil: %v %v response condition not yet met, retrying", method, req.URL)
570+
// small sleep to avoid tight-looping
571+
time.Sleep(100 * time.Millisecond)
518572
}
519-
return res
520573
}
521574

522575
// NewLoggedClient returns an http.Client which logs requests/responses

0 commit comments

Comments
 (0)