commands/helpers/retry_helper.go (73 lines of code) (raw):
package helpers
import (
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/sirupsen/logrus"
)
// Cloud Providers supported currently send error in case of HTTP API request failure in XML Format
// The Format spec is the same for:
// GCS: https://cloud.google.com/storage/docs/xml-api/reference-status
// AWS S3: https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#RESTErrorResponses
// and Azure Blob Storage: https://learn.microsoft.com/en-us/rest/api/storageservices/status-and-error-codes2
// storageErrorResponse is used to deserialize such error responses and provide better error failures message in the log.
type storageErrorResponse struct {
XMLName xml.Name `xml:"Error"`
Code string `xml:"Code"`
Message string `xml:"Message"`
}
func (ser *storageErrorResponse) isValid() bool {
return ser.Code != "" || ser.Message != ""
}
func (ser *storageErrorResponse) String() string {
if !ser.isValid() {
return ""
}
msg := ""
if ser.Code != "" {
msg = "code: " + ser.Code
}
if ser.Message != "" {
msg += ", message: " + ser.Message
}
return msg
}
type retryHelper struct {
Retry int `long:"retry" description:"How many times to retry upload"`
RetryTime time.Duration `long:"retry-time" description:"How long to wait between retries"`
}
// retryableErr indicates that an error can be retried. To specify that an error
// can be retried simply wrap the original error. For example:
//
// retryableErr{err: errors.New("some error")}
type retryableErr struct {
err error
}
func (e retryableErr) Unwrap() error {
return e.err
}
func (e retryableErr) Error() string {
return e.err.Error()
}
func (r *retryHelper) doRetry(handler func(int) error) error {
err := handler(0)
for retry := 1; retry <= r.Retry; retry++ {
if _, ok := err.(retryableErr); !ok {
return err
}
time.Sleep(r.RetryTime)
logrus.WithError(err).Warningln("Retrying...")
err = handler(retry)
}
return err
}
// retryOnServerError will take the response and check if the the error should
// be of type retryableErr or not. When the status code is of 5xx it will be a
// retryableErr.
func retryOnServerError(resp *http.Response) error {
if resp.StatusCode/100 == 2 {
return nil
}
errResp := &storageErrorResponse{}
bodyBytes, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
errMsg := fmt.Sprintf("received: %s", resp.Status)
if err := xml.Unmarshal(bodyBytes, errResp); err == nil && errResp.isValid() {
errMsg = fmt.Sprintf("%s. Request failed with %s", errMsg, errResp.String())
}
err := errors.New(errMsg)
if resp.StatusCode/100 == 5 {
err = retryableErr{err: err}
}
return err
}