|
- // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
- // resty source code and usage is governed by a MIT style
- // license that can be found in the LICENSE file.
-
- package resty
-
- import (
- "context"
- "math"
- "math/rand"
- "time"
- )
-
- const (
- defaultMaxRetries = 3
- defaultWaitTime = time.Duration(100) * time.Millisecond
- defaultMaxWaitTime = time.Duration(2000) * time.Millisecond
- )
-
- type (
- // Option is to create convenient retry options like wait time, max retries, etc.
- Option func(*Options)
-
- // RetryConditionFunc type is for retry condition function
- // input: non-nil Response OR request execution error
- RetryConditionFunc func(*Response, error) bool
-
- // RetryAfterFunc returns time to wait before retry
- // For example, it can parse HTTP Retry-After header
- // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
- // Non-nil error is returned if it is found that request is not retryable
- // (0, nil) is a special result means 'use default algorithm'
- RetryAfterFunc func(*Client, *Response) (time.Duration, error)
-
- // Options struct is used to hold retry settings.
- Options struct {
- maxRetries int
- waitTime time.Duration
- maxWaitTime time.Duration
- retryConditions []RetryConditionFunc
- }
- )
-
- // Retries sets the max number of retries
- func Retries(value int) Option {
- return func(o *Options) {
- o.maxRetries = value
- }
- }
-
- // WaitTime sets the default wait time to sleep between requests
- func WaitTime(value time.Duration) Option {
- return func(o *Options) {
- o.waitTime = value
- }
- }
-
- // MaxWaitTime sets the max wait time to sleep between requests
- func MaxWaitTime(value time.Duration) Option {
- return func(o *Options) {
- o.maxWaitTime = value
- }
- }
-
- // RetryConditions sets the conditions that will be checked for retry.
- func RetryConditions(conditions []RetryConditionFunc) Option {
- return func(o *Options) {
- o.retryConditions = conditions
- }
- }
-
- // Backoff retries with increasing timeout duration up until X amount of retries
- // (Default is 3 attempts, Override with option Retries(n))
- func Backoff(operation func() (*Response, error), options ...Option) error {
- // Defaults
- opts := Options{
- maxRetries: defaultMaxRetries,
- waitTime: defaultWaitTime,
- maxWaitTime: defaultMaxWaitTime,
- retryConditions: []RetryConditionFunc{},
- }
-
- for _, o := range options {
- o(&opts)
- }
-
- var (
- resp *Response
- err error
- )
-
- for attempt := 0; attempt <= opts.maxRetries; attempt++ {
- resp, err = operation()
- ctx := context.Background()
- if resp != nil && resp.Request.ctx != nil {
- ctx = resp.Request.ctx
- }
- if ctx.Err() != nil {
- return err
- }
-
- err1 := unwrapNoRetryErr(err) // raw error, it used for return users callback.
- needsRetry := err != nil && err == err1 // retry on a few operation errors by default
-
- for _, condition := range opts.retryConditions {
- needsRetry = condition(resp, err1)
- if needsRetry {
- break
- }
- }
-
- if !needsRetry {
- return err
- }
-
- waitTime, err2 := sleepDuration(resp, opts.waitTime, opts.maxWaitTime, attempt)
- if err2 != nil {
- if err == nil {
- err = err2
- }
- return err
- }
-
- select {
- case <-time.After(waitTime):
- case <-ctx.Done():
- return ctx.Err()
- }
- }
-
- return err
- }
-
- func sleepDuration(resp *Response, min, max time.Duration, attempt int) (time.Duration, error) {
- const maxInt = 1<<31 - 1 // max int for arch 386
-
- if max < 0 {
- max = maxInt
- }
-
- if resp == nil {
- goto defaultCase
- }
-
- // 1. Check for custom callback
- if retryAfterFunc := resp.Request.client.RetryAfter; retryAfterFunc != nil {
- result, err := retryAfterFunc(resp.Request.client, resp)
- if err != nil {
- return 0, err // i.e. 'API quota exceeded'
- }
- if result == 0 {
- goto defaultCase
- }
- if result < 0 || max < result {
- result = max
- }
- if result < min {
- result = min
- }
- return result, nil
- }
-
- // 2. Return capped exponential backoff with jitter
- // http://www.awsarchitectureblog.com/2015/03/backoff.html
- defaultCase:
- base := float64(min)
- capLevel := float64(max)
-
- temp := math.Min(capLevel, base*math.Exp2(float64(attempt)))
- ri := int(temp / 2)
- if ri <= 0 {
- ri = maxInt // max int for arch 386
- }
- result := time.Duration(math.Abs(float64(ri + rand.Intn(ri))))
-
- if result < min {
- result = min
- }
-
- return result, nil
- }
|