Skip to content

Commit

Permalink
Add WithRetryUntil request opt which can be used with any CSAPI call
Browse files Browse the repository at this point in the history
This will repeat a request until the condition is satisfied.

Intended use: #415
  • Loading branch information
kegsay committed Jul 21, 2022
1 parent 4418d2c commit 371d153
Showing 1 changed file with 65 additions and 12 deletions.
77 changes: 65 additions & 12 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package client

import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha1" // nolint:gosec
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httputil"
Expand All @@ -27,6 +29,17 @@ const (
SharedSecret = "complement"
)

type CtxKey string

const (
CtxKeyWithRetryUntil CtxKey = "complement_retry_until" // contains *retryUntilParams
)

type retryUntilParams struct {
timeout time.Duration
untilFn func(*http.Response) bool
}

// RequestOpt is a functional option which will modify an outgoing HTTP request.
// See functions starting with `With...` in this package for more info.
type RequestOpt func(req *http.Request)
Expand Down Expand Up @@ -441,6 +454,16 @@ func WithQueries(q url.Values) RequestOpt {
}
}

// WithRetryUntil will retry the request until the provided function returns true. Times out after
// `timeout`, which will then fail the test.
func WithRetryUntil(timeout time.Duration, untilFn func(res *http.Response) bool) RequestOpt {
return func(req *http.Request) {
until := req.Context().Value(CtxKeyWithRetryUntil).(*retryUntilParams)
until.timeout = timeout
until.untilFn = untilFn
}
}

// MustDoFunc is the same as DoFunc but fails the test if the returned HTTP response code is not 2xx.
func (c *CSAPI) MustDoFunc(t *testing.T, method string, paths []string, opts ...RequestOpt) *http.Response {
t.Helper()
Expand Down Expand Up @@ -479,6 +502,9 @@ func (c *CSAPI) DoFunc(t *testing.T, method string, paths []string, opts ...Requ
if c.AccessToken != "" {
req.Header.Set("Authorization", "Bearer "+c.AccessToken)
}
retryUntil := &retryUntilParams{}
ctx := context.WithValue(req.Context(), CtxKeyWithRetryUntil, retryUntil)
req = req.WithContext(ctx)

// set functional options
for _, o := range opts {
Expand All @@ -502,21 +528,48 @@ func (c *CSAPI) DoFunc(t *testing.T, method string, paths []string, opts ...Requ
t.Logf("Request body: <binary:%s>", contentType)
}
}
// Perform the HTTP request
res, err := c.Client.Do(req)
if err != nil {
t.Fatalf("CSAPI.DoFunc response returned error: %s", err)
}
// debug log the response
if c.Debug && res != nil {
var dump []byte
dump, err = httputil.DumpResponse(res, true)
now := time.Now()
for {
// Perform the HTTP request
res, err := c.Client.Do(req)
if err != nil {
t.Fatalf("CSAPI.DoFunc failed to dump response body: %s", err)
t.Fatalf("CSAPI.DoFunc response returned error: %s", err)
}
// debug log the response
if c.Debug && res != nil {
var dump []byte
dump, err = httputil.DumpResponse(res, true)
if err != nil {
t.Fatalf("CSAPI.DoFunc failed to dump response body: %s", err)
}
t.Logf("%s", string(dump))
}
if retryUntil == nil || retryUntil.timeout == 0 {
return res // don't retry
}

// check the condition, make a copy of the response body first in case the check consumes it
var resBody []byte
if res.Body != nil {
resBody, err = ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("CSAPI.DoFunc failed to read response body for RetryUntil check: %s", err)
}
res.Body = io.NopCloser(bytes.NewBuffer(resBody))
}
t.Logf("%s", string(dump))
if retryUntil.untilFn(res) {
// remake the response and return
res.Body = io.NopCloser(bytes.NewBuffer(resBody))
return res
}
// condition not satisfied, do we timeout yet?
if time.Since(now) > retryUntil.timeout {
t.Fatalf("CSAPI.DoFunc RetryUntil: %v %v timed out after %v", method, req.URL, retryUntil.timeout)
}
t.Logf("CSAPI.DoFunc RetryUntil: %v %v response condition not yet met, retrying", method, req.URL)
// small sleep to avoid tight-looping
time.Sleep(100 * time.Millisecond)
}
return res
}

// NewLoggedClient returns an http.Client which logs requests/responses
Expand Down

0 comments on commit 371d153

Please sign in to comment.