oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/registry/remote/retry/policy.go (about)

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package retry
    17  
    18  import (
    19  	"hash/maphash"
    20  	"math"
    21  	"math/rand"
    22  	"net"
    23  	"net/http"
    24  	"strconv"
    25  	"time"
    26  )
    27  
    28  // headerRetryAfter is the header key for Retry-After.
    29  const headerRetryAfter = "Retry-After"
    30  
    31  // DefaultPolicy is a policy with fine-tuned retry parameters.
    32  // It uses an exponential backoff with jitter.
    33  var DefaultPolicy Policy = &GenericPolicy{
    34  	Retryable: DefaultPredicate,
    35  	Backoff:   DefaultBackoff,
    36  	MinWait:   200 * time.Millisecond,
    37  	MaxWait:   3 * time.Second,
    38  	MaxRetry:  5,
    39  }
    40  
    41  // DefaultPredicate is a predicate that retries on 5xx errors, 429 Too Many
    42  // Requests, 408 Request Timeout and on network dial timeout.
    43  var DefaultPredicate Predicate = func(resp *http.Response, err error) (bool, error) {
    44  	if err != nil {
    45  		// retry on Dial timeout
    46  		if err, ok := err.(net.Error); ok && err.Timeout() {
    47  			return true, nil
    48  		}
    49  		return false, err
    50  	}
    51  
    52  	if resp.StatusCode == http.StatusRequestTimeout || resp.StatusCode == http.StatusTooManyRequests {
    53  		return true, nil
    54  	}
    55  
    56  	if resp.StatusCode == 0 || resp.StatusCode >= 500 {
    57  		return true, nil
    58  	}
    59  
    60  	return false, nil
    61  }
    62  
    63  // DefaultBackoff is a backoff that uses an exponential backoff with jitter.
    64  // It uses a base of 250ms, a factor of 2 and a jitter of 10%.
    65  var DefaultBackoff Backoff = ExponentialBackoff(250*time.Millisecond, 2, 0.1)
    66  
    67  // Policy is a retry policy.
    68  type Policy interface {
    69  	// Retry returns the duration to wait before retrying the request.
    70  	// It returns a negative value if the request should not be retried.
    71  	// The attempt is used to:
    72  	//  - calculate the backoff duration, the default backoff is an exponential backoff.
    73  	//  - determine if the request should be retried.
    74  	// The attempt starts at 0 and should be less than MaxRetry for the request to
    75  	// be retried.
    76  	Retry(attempt int, resp *http.Response, err error) (time.Duration, error)
    77  }
    78  
    79  // Predicate is a function that returns true if the request should be retried.
    80  type Predicate func(resp *http.Response, err error) (bool, error)
    81  
    82  // Backoff is a function that returns the duration to wait before retrying the
    83  // request. The attempt, is the next attempt number. The response is the
    84  // response from the previous request.
    85  type Backoff func(attempt int, resp *http.Response) time.Duration
    86  
    87  // ExponentialBackoff returns a Backoff that uses an exponential backoff with
    88  // jitter. The backoff is calculated as:
    89  //
    90  //	temp = backoff * factor ^ attempt
    91  //	interval = temp * (1 - jitter) + rand.Int63n(2 * jitter * temp)
    92  //
    93  // The HTTP response is checked for a Retry-After header. If it is present, the
    94  // value is used as the backoff duration.
    95  func ExponentialBackoff(backoff time.Duration, factor, jitter float64) Backoff {
    96  	return func(attempt int, resp *http.Response) time.Duration {
    97  		var h maphash.Hash
    98  		h.SetSeed(maphash.MakeSeed())
    99  		rand := rand.New(rand.NewSource(int64(h.Sum64())))
   100  
   101  		// check Retry-After
   102  		if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
   103  			if v := resp.Header.Get(headerRetryAfter); v != "" {
   104  				if retryAfter, _ := strconv.ParseInt(v, 10, 64); retryAfter > 0 {
   105  					return time.Duration(retryAfter) * time.Second
   106  				}
   107  			}
   108  		}
   109  
   110  		// do exponential backoff with jitter
   111  		temp := float64(backoff) * math.Pow(factor, float64(attempt))
   112  		return time.Duration(temp*(1-jitter)) + time.Duration(rand.Int63n(int64(2*jitter*temp)))
   113  	}
   114  }
   115  
   116  // GenericPolicy is a generic retry policy.
   117  type GenericPolicy struct {
   118  	// Retryable is a predicate that returns true if the request should be
   119  	// retried.
   120  	Retryable Predicate
   121  
   122  	// Backoff is a function that returns the duration to wait before retrying.
   123  	Backoff Backoff
   124  
   125  	// MinWait is the minimum duration to wait before retrying.
   126  	MinWait time.Duration
   127  
   128  	// MaxWait is the maximum duration to wait before retrying.
   129  	MaxWait time.Duration
   130  
   131  	// MaxRetry is the maximum number of retries.
   132  	MaxRetry int
   133  }
   134  
   135  // Retry returns the duration to wait before retrying the request.
   136  // It returns -1 if the request should not be retried.
   137  func (p *GenericPolicy) Retry(attempt int, resp *http.Response, err error) (time.Duration, error) {
   138  	if attempt >= p.MaxRetry {
   139  		return -1, nil
   140  	}
   141  	if ok, err := p.Retryable(resp, err); err != nil {
   142  		return -1, err
   143  	} else if !ok {
   144  		return -1, nil
   145  	}
   146  	backoff := p.Backoff(attempt, resp)
   147  	if backoff < p.MinWait {
   148  		backoff = p.MinWait
   149  	}
   150  	if backoff > p.MaxWait {
   151  		backoff = p.MaxWait
   152  	}
   153  	return backoff, nil
   154  }