diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go index a838c706a..d517c8f40 100644 --- a/pkg/retry/retry.go +++ b/pkg/retry/retry.go @@ -17,8 +17,9 @@ import ( // Options defines the option to retry. type Options struct { - MaxRetry int // The number of times to possibly retry. - Delay time.Duration // The delay to use between retries, if set. + MaxRetry int // The number of times to possibly retry. + Delay time.Duration // The delay to use between retries, if set. + IsErrorRetryable func(error) bool } // RetryOptions is deprecated, use Options. @@ -31,8 +32,11 @@ func RetryIfNecessary(ctx context.Context, operation func() error, options *Opti // IfNecessary retries the operation in exponential backoff with the retry Options. func IfNecessary(ctx context.Context, operation func() error, options *Options) error { + if options.IsErrorRetryable == nil { + options.IsErrorRetryable = IsErrorRetryable + } err := operation() - for attempt := 0; err != nil && isRetryable(err) && attempt < options.MaxRetry; attempt++ { + for attempt := 0; err != nil && options.IsErrorRetryable(err) && attempt < options.MaxRetry; attempt++ { delay := time.Duration(int(math.Pow(2, float64(attempt)))) * time.Second if options.Delay != 0 { delay = options.Delay @@ -49,7 +53,11 @@ func IfNecessary(ctx context.Context, operation func() error, options *Options) return err } -func isRetryable(err error) bool { +// IsErrorRetryable makes a HEURISTIC determination whether it is worth retrying upon encountering an error. +// That heuristic is NOT STABLE and it CAN CHANGE AT ANY TIME. +// Callers that have a hard requirement for specific treatment of a class of errors should make their own check +// instead of relying on this function maintaining its past behavior. +func IsErrorRetryable(err error) bool { switch err { case nil: return false @@ -72,18 +80,18 @@ func isRetryable(err error) bool { } return true case *net.OpError: - return isRetryable(e.Err) + return IsErrorRetryable(e.Err) case *url.Error: // This includes errors returned by the net/http client. if e.Err == io.EOF { // Happens when a server accepts a HTTP connection and sends EOF return true } - return isRetryable(e.Err) + return IsErrorRetryable(e.Err) case syscall.Errno: return isErrnoRetryable(e) case errcode.Errors: // if this error is a group of errors, process them all in turn for i := range e { - if !isRetryable(e[i]) { + if !IsErrorRetryable(e[i]) { return false } } @@ -91,7 +99,7 @@ func isRetryable(err error) bool { case *multierror.Error: // if this error is a group of errors, process them all in turn for i := range e.Errors { - if !isRetryable(e.Errors[i]) { + if !IsErrorRetryable(e.Errors[i]) { return false } } @@ -102,11 +110,11 @@ func isRetryable(err error) bool { } if unwrappable, ok := e.(unwrapper); ok { err = unwrappable.Unwrap() - return isRetryable(err) + return IsErrorRetryable(err) } case unwrapper: // Test this last, because various error types might implement .Unwrap() err = e.Unwrap() - return isRetryable(err) + return IsErrorRetryable(err) } return false