1// Copyright (c) 2015-2019 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
2// resty source code and usage is governed by a MIT style
3// license that can be found in the LICENSE file.
4
5package resty
6
7import (
8	"math"
9	"math/rand"
10	"time"
11)
12
13const (
14	defaultMaxRetries  = 3
15	defaultWaitTime    = time.Duration(100) * time.Millisecond
16	defaultMaxWaitTime = time.Duration(2000) * time.Millisecond
17)
18
19type (
20	// Option is to create convenient retry options like wait time, max retries, etc.
21	Option func(*Options)
22
23	// RetryConditionFunc type is for retry condition function
24	RetryConditionFunc func(*Response) (bool, error)
25
26	// Options to hold go-resty retry values
27	Options struct {
28		maxRetries      int
29		waitTime        time.Duration
30		maxWaitTime     time.Duration
31		retryConditions []RetryConditionFunc
32	}
33)
34
35// Retries sets the max number of retries
36func Retries(value int) Option {
37	return func(o *Options) {
38		o.maxRetries = value
39	}
40}
41
42// WaitTime sets the default wait time to sleep between requests
43func WaitTime(value time.Duration) Option {
44	return func(o *Options) {
45		o.waitTime = value
46	}
47}
48
49// MaxWaitTime sets the max wait time to sleep between requests
50func MaxWaitTime(value time.Duration) Option {
51	return func(o *Options) {
52		o.maxWaitTime = value
53	}
54}
55
56// RetryConditions sets the conditions that will be checked for retry.
57func RetryConditions(conditions []RetryConditionFunc) Option {
58	return func(o *Options) {
59		o.retryConditions = conditions
60	}
61}
62
63// Backoff retries with increasing timeout duration up until X amount of retries
64// (Default is 3 attempts, Override with option Retries(n))
65func Backoff(operation func() (*Response, error), options ...Option) error {
66	// Defaults
67	opts := Options{
68		maxRetries:      defaultMaxRetries,
69		waitTime:        defaultWaitTime,
70		maxWaitTime:     defaultMaxWaitTime,
71		retryConditions: []RetryConditionFunc{},
72	}
73
74	for _, o := range options {
75		o(&opts)
76	}
77
78	var (
79		resp *Response
80		err  error
81	)
82	base := float64(opts.waitTime)        // Time to wait between each attempt
83	capLevel := float64(opts.maxWaitTime) // Maximum amount of wait time for the retry
84	for attempt := 0; attempt < opts.maxRetries; attempt++ {
85		resp, err = operation()
86
87		var needsRetry bool
88		var conditionErr error
89		for _, condition := range opts.retryConditions {
90			needsRetry, conditionErr = condition(resp)
91			if needsRetry || conditionErr != nil {
92				break
93			}
94		}
95
96		// If the operation returned no error, there was no condition satisfied and
97		// there was no error caused by the conditional functions.
98		if err == nil && !needsRetry && conditionErr == nil {
99			return nil
100		}
101		// Adding capped exponential backup with jitter
102		// See the following article...
103		// http://www.awsarchitectureblog.com/2015/03/backoff.html
104		temp := math.Min(capLevel, base*math.Exp2(float64(attempt)))
105		ri := int(temp / 2)
106		if ri <= 0 {
107			ri = 1<<31 - 1 // max int for arch 386
108		}
109		sleepDuration := time.Duration(math.Abs(float64(ri + rand.Intn(ri))))
110
111		if sleepDuration < opts.waitTime {
112			sleepDuration = opts.waitTime
113		}
114		time.Sleep(sleepDuration)
115	}
116
117	return err
118}
119