Skip to content

Commit

Permalink
Adding min and max delay for retry (#149)
Browse files Browse the repository at this point in the history
  • Loading branch information
bendbennett committed Feb 3, 2023
1 parent 627dc13 commit 6f95f1f
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 13 deletions.
50 changes: 41 additions & 9 deletions internal/provider/data_source_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,21 @@ your control should be treated as untrustworthy.`,
int64validator.AtLeast(0),
},
},
"min_delay": schema.Int64Attribute{
Description: "The minimum delay between retry requests in milliseconds.",
Optional: true,
Validators: []validator.Int64{
int64validator.AtLeast(0),
},
},
"max_delay": schema.Int64Attribute{
Description: "The maximum delay between retry requests in milliseconds.",
Optional: true,
Validators: []validator.Int64{
int64validator.AtLeast(0),
int64validator.AtLeastSumOf(path.MatchRelative().AtParent().AtName("min_delay")),
},
},
},
},
},
Expand Down Expand Up @@ -234,19 +249,28 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
}
}

timeout := model.RequestTimeout

retryClient := retryablehttp.NewClient()
retryClient.HTTPClient.Transport = clonedTr
retryClient.HTTPClient.Timeout = time.Duration(timeout.ValueInt64()) * time.Millisecond

var timeout time.Duration

if model.RequestTimeout.ValueInt64() > 0 {
timeout = time.Duration(model.RequestTimeout.ValueInt64()) * time.Millisecond
retryClient.HTTPClient.Timeout = timeout
}

retryClient.Logger = levelledLogger{ctx}
retryClient.RetryMax = int(retry.Attempts.ValueInt64())
retryClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
if err == nil {
return false, nil
}

return true, fmt.Errorf("request generated error: %w", err)
retryMinDelay := retry.MinDelay.ValueInt64()
retryMaxDelay := retry.MaxDelay.ValueInt64()

if retryMinDelay > 0 {
retryClient.RetryWaitMin = time.Duration(retryMinDelay) * time.Millisecond
}

if retryMaxDelay > 0 {
retryClient.RetryWaitMax = time.Duration(retryMaxDelay) * time.Millisecond
}

request, err := retryablehttp.NewRequestWithContext(ctx, method, requestURL, requestBody)
Expand Down Expand Up @@ -274,9 +298,15 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
target := &url.Error{}
if errors.As(err, &target) {
if target.Timeout() {
detail := fmt.Sprintf("timeout error: %s", err)

if timeout > 0 {
detail = fmt.Sprintf("request exceeded the specified timeout: %s, err: %s", timeout.String(), err)
}

resp.Diagnostics.AddError(
"Error making request",
fmt.Sprintf("%s: request exceeded the specified timeout: %d ms", err, timeout.ValueInt64()),
detail,
)
return
}
Expand Down Expand Up @@ -376,6 +406,8 @@ type modelV0 struct {

type retryModel struct {
Attempts types.Int64 `tfsdk:"attempts"`
MinDelay types.Int64 `tfsdk:"min_delay"`
MaxDelay types.Int64 `tfsdk:"max_delay"`
}

var _ retryablehttp.LeveledLogger = levelledLogger{}
Expand Down
134 changes: 130 additions & 4 deletions internal/provider/data_source_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ func TestDataSource_InsecureFalse(t *testing.T) {
ExpectError: regexp.MustCompile(
fmt.Sprintf(
"Error making request: GET %s giving up after 1\n"+
"attempt\\(s\\): request generated error: Get \"%s\": x509:",
"attempt\\(s\\): Get \"%s\": x509:",
testServer.URL,
testServer.URL,
),
Expand All @@ -528,7 +528,7 @@ func TestDataSource_InsecureUnconfigured(t *testing.T) {
ExpectError: regexp.MustCompile(
fmt.Sprintf(
"Error making request: GET %s giving up after 1\n"+
"attempt\\(s\\): request generated error: Get \"%s\": x509:",
"attempt\\(s\\): Get \"%s\": x509:",
testServer.URL,
testServer.URL,
),
Expand Down Expand Up @@ -633,7 +633,7 @@ func TestDataSource_Timeout(t *testing.T) {
url = "%s"
request_timeout = 5
}`, svr.URL),
ExpectError: regexp.MustCompile(`request exceeded the\nspecified timeout: 5 ms`),
ExpectError: regexp.MustCompile(`request exceeded the specified timeout: 5ms`),
},
},
})
Expand All @@ -656,7 +656,7 @@ func TestDataSource_Retry(t *testing.T) {
ExpectError: regexp.MustCompile(
fmt.Sprintf(
"Error making request: GET https://%s.com\n"+
"giving up after 2 attempt\\(s\\): request generated error: Get\n"+
"giving up after 2 attempt\\(s\\): Get\n"+
"\"https://%s.com\": dial tcp: lookup\n"+
"%s.com: no such host",
uid.String(), uid.String(), uid.String(),
Expand All @@ -667,6 +667,104 @@ func TestDataSource_Retry(t *testing.T) {
})
}

func TestDataSource_MinDelay(t *testing.T) {
var timeOfFirstRequest, timeOfSecondRequest int64
minDelay := 200

svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")

if timeOfFirstRequest == 0 {
timeOfFirstRequest = time.Now().UnixNano() / int64(time.Millisecond)
w.WriteHeader(http.StatusBadGateway)
} else {
timeOfSecondRequest = time.Now().UnixNano() / int64(time.Millisecond)
}
}))
defer svr.Close()

resource.ParallelTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
data "http" "http_test" {
url = "%s"
retry {
attempts = 1
min_delay = %d
}
}`, svr.URL, minDelay),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.http.http_test", "status_code", "200"),
checkMinDelay(&timeOfFirstRequest, &timeOfSecondRequest, minDelay),
),
},
},
})
}

func TestDataSource_MaxDelay(t *testing.T) {
var timeOfFirstRequest, timeOfSecondRequest int64
maxDelay := 200

svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")

if timeOfFirstRequest == 0 {
timeOfFirstRequest = time.Now().UnixNano() / int64(time.Millisecond)
w.WriteHeader(http.StatusBadGateway)
} else {
timeOfSecondRequest = time.Now().UnixNano() / int64(time.Millisecond)
}
}))
defer svr.Close()

resource.ParallelTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
data "http" "http_test" {
url = "%s"
retry {
attempts = 1
max_delay = %d
}
}`, svr.URL, maxDelay),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.http.http_test", "status_code", "200"),
checkMaxDelay(&timeOfFirstRequest, &timeOfSecondRequest, maxDelay, 100),
),
},
},
})
}

func TestDataSource_MaxDelayAtLeastEqualToMinDelay(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
}))
defer svr.Close()

resource.ParallelTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
data "http" "http_test" {
url = "%s"
retry {
attempts = 1
min_delay = 300
max_delay = 200
}
}`, svr.URL),
ExpectError: regexp.MustCompile("Attribute retry.max_delay value must be at least sum of <.min_delay, got: 200"),
},
},
})
}

func checkServerAndProxyRequestCount(proxyRequestCount, serverRequestCount *int) resource.TestCheckFunc {
return func(_ *terraform.State) error {
if *proxyRequestCount != *serverRequestCount {
Expand All @@ -683,3 +781,31 @@ func certToPEM(cert *x509.Certificate) string {

return strings.Trim(certPem, "\n")
}

func checkMinDelay(timeOfFirstRequest, timeOfSecondRequest *int64, minDelay int) resource.TestCheckFunc {
return func(_ *terraform.State) error {
if *timeOfFirstRequest != *timeOfSecondRequest {
diff := *timeOfSecondRequest - *timeOfFirstRequest

if diff < int64(minDelay) {
return fmt.Errorf("expected delay between requests to be at least: %dms, was actually: %dms", minDelay, diff)
}
}

return nil
}
}

func checkMaxDelay(timeOfFirstRequest, timeOfSecondRequest *int64, maxDelay, allowedRequestDuration int) resource.TestCheckFunc {
return func(_ *terraform.State) error {
if *timeOfFirstRequest != *timeOfSecondRequest {
diff := *timeOfSecondRequest - *timeOfFirstRequest

if diff > int64(maxDelay+allowedRequestDuration) {
return fmt.Errorf("expected delay between requests to be at most: %dms, was actually: %dms", maxDelay+allowedRequestDuration, diff)
}
}

return nil
}
}

0 comments on commit 6f95f1f

Please sign in to comment.