diff --git a/internal/acceptance_test/helper.go b/internal/acceptance_test/helper.go index 69f42d33..c3137c91 100644 --- a/internal/acceptance_test/helper.go +++ b/internal/acceptance_test/helper.go @@ -6,7 +6,6 @@ import ( "context" "fmt" "log" - "net/http" "os" "strconv" "time" @@ -58,16 +57,16 @@ func getAPIClient() (*api_client.APIClient, api_client.Configuration) { if err != nil { log.Printf("[ERROR] Error getting cmp details: %s", err) } - tr := &http.Transport{ - MaxIdleConns: 20, - MaxIdleConnsPerHost: 20, - DisableKeepAlives: true, - } + // tr := &http.Transport{ + // MaxIdleConns: 20, + // MaxIdleConnsPerHost: 20, + // DisableKeepAlives: true, + // } cfg := api_client.Configuration{ Host: cmpDetails.URL, DefaultHeader: map[string]string{}, DefaultQueryParams: map[string]string{}, - HTTPClient: &http.Client{Transport: tr, Timeout: 2 * time.Minute}, + HTTPClient: utils.NewRetryableClient(), } cmpAPIClient := api_client.NewAPIClient(&cfg) cmpAPIClient.CMPToken = cmpDetails.AccessToken diff --git a/pkg/client/client.go b/pkg/client/client.go index b2653610..ce21f7b6 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -4,9 +4,7 @@ package client import ( "fmt" - "net/http" "os" - "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -74,16 +72,16 @@ func (i InitialiseClient) NewClient(r *schema.ResourceData) (interface{}, error) brokerHeaders := getHeaders() tenantID := r.Get(constants.TenantID).(string) brokerHeaders["X-Tenant-ID"] = tenantID - tr := &http.Transport{ - MaxIdleConns: 20, - MaxIdleConnsPerHost: 20, - DisableKeepAlives: true, - } + // tr := &http.Transport{ + // MaxIdleConns: 20, + // MaxIdleConnsPerHost: 20, + // DisableKeepAlives: true, + // } brokerCfgForAPIClient := api_client.Configuration{ Host: vmaasProviderSettings[constants.BROKERRURL].(string), DefaultHeader: brokerHeaders, DefaultQueryParams: queryParam, - HTTPClient: &http.Client{Transport: tr, Timeout: 2 * time.Minute}, + HTTPClient: utils.NewRetryableClient(), } brokerApiClient := api_client.NewAPIClient(&brokerCfgForAPIClient) utils.SetMetaFnAndVersion(brokerApiClient, r, 0) @@ -92,7 +90,7 @@ func (i InitialiseClient) NewClient(r *schema.ResourceData) (interface{}, error) Host: "", DefaultHeader: map[string]string{}, DefaultQueryParams: map[string]string{}, - HTTPClient: &http.Client{Transport: tr, Timeout: 2 * time.Minute}, + HTTPClient: utils.NewRetryableClient(), } apiClient := api_client.NewAPIClient(&cfg) err = utils.SetCMPVars(apiClient, brokerApiClient, &cfg) diff --git a/pkg/utils/http_retry.go b/pkg/utils/http_retry.go new file mode 100644 index 00000000..9857e5a3 --- /dev/null +++ b/pkg/utils/http_retry.go @@ -0,0 +1,87 @@ +package utils + +import ( + "bytes" + "io" + "math" + "net/http" + "time" +) + +const RetryCount = 3 + +type retryableTransport struct { + transport http.RoundTripper +} + +func backoff(retries int) time.Duration { + return time.Duration(math.Pow(2, float64(retries))) * time.Second +} + +func shouldRetry(err error, resp *http.Response) bool { + if err != nil { + return true + } + + if resp.StatusCode == 401 { + return true + } + + return false +} + +func drainBody(resp *http.Response) { + if resp.Body != nil { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } +} + +func (t *retryableTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Clone the request body + var bodyBytes []byte + if req.Body != nil { + bodyBytes, _ = io.ReadAll(req.Body) + req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + + // Send the request + resp, err := t.transport.RoundTrip(req) + + // Retry logic + retries := 0 + for shouldRetry(err, resp) && retries < RetryCount { + // Wait for the specified backoff period + time.Sleep(backoff(retries)) + + // We're going to retry, consume any response to reuse the connection. + drainBody(resp) + + // Clone the request body again + if req.Body != nil { + req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + + // Retry the request + resp, err = t.transport.RoundTrip(req) + + retries++ + } + + // Return the response + return resp, err +} + +func NewRetryableClient() *http.Client { + transport := &retryableTransport{ + transport: &http.Transport{ + MaxIdleConns: 20, + MaxIdleConnsPerHost: 20, + DisableKeepAlives: true}, + } + + return &http.Client{ + Transport: transport, + Timeout: 2 * time.Minute, + } +}