github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/retry/retry.go (about) 1 // Copyright 2014 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package retry 12 13 import ( 14 "context" 15 "math" 16 "math/rand" 17 "time" 18 19 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 20 "github.com/cockroachdb/errors" 21 ) 22 23 // Options provides reusable configuration of Retry objects. 24 type Options struct { 25 InitialBackoff time.Duration // Default retry backoff interval 26 MaxBackoff time.Duration // Maximum retry backoff interval 27 Multiplier float64 // Default backoff constant 28 MaxRetries int // Maximum number of attempts (0 for infinite) 29 RandomizationFactor float64 // Randomize the backoff interval by constant 30 Closer <-chan struct{} // Optionally end retry loop channel close. 31 } 32 33 // Retry implements the public methods necessary to control an exponential- 34 // backoff retry loop. 35 type Retry struct { 36 opts Options 37 ctxDoneChan <-chan struct{} 38 currentAttempt int 39 isReset bool 40 } 41 42 // Start returns a new Retry initialized to some default values. The Retry can 43 // then be used in an exponential-backoff retry loop. 44 func Start(opts Options) Retry { 45 return StartWithCtx(context.Background(), opts) 46 } 47 48 // StartWithCtx returns a new Retry initialized to some default values. The 49 // Retry can then be used in an exponential-backoff retry loop. If the provided 50 // context is canceled (see Context.Done), the retry loop ends early. 51 func StartWithCtx(ctx context.Context, opts Options) Retry { 52 if opts.InitialBackoff == 0 { 53 opts.InitialBackoff = 50 * time.Millisecond 54 } 55 if opts.MaxBackoff == 0 { 56 opts.MaxBackoff = 2 * time.Second 57 } 58 if opts.RandomizationFactor == 0 { 59 opts.RandomizationFactor = 0.15 60 } 61 if opts.Multiplier == 0 { 62 opts.Multiplier = 2 63 } 64 65 r := Retry{opts: opts} 66 r.ctxDoneChan = ctx.Done() 67 r.Reset() 68 return r 69 } 70 71 // Reset resets the Retry to its initial state, meaning that the next call to 72 // Next will return true immediately and subsequent calls will behave as if 73 // they had followed the very first attempt (i.e. their backoffs will be 74 // short). 75 func (r *Retry) Reset() { 76 select { 77 case <-r.opts.Closer: 78 // When the closer has fired, you can't keep going. 79 return 80 case <-r.ctxDoneChan: 81 // When the context was canceled, you can't keep going. 82 return 83 default: 84 } 85 r.currentAttempt = 0 86 r.isReset = true 87 } 88 89 func (r Retry) retryIn() time.Duration { 90 backoff := float64(r.opts.InitialBackoff) * math.Pow(r.opts.Multiplier, float64(r.currentAttempt)) 91 if maxBackoff := float64(r.opts.MaxBackoff); backoff > maxBackoff { 92 backoff = maxBackoff 93 } 94 95 var delta = r.opts.RandomizationFactor * backoff 96 // Get a random value from the range [backoff - delta, backoff + delta]. 97 // The formula used below has a +1 because time.Duration is an int64, and the 98 // conversion floors the float64. 99 return time.Duration(backoff - delta + rand.Float64()*(2*delta+1)) 100 } 101 102 // Next returns whether the retry loop should continue, and blocks for the 103 // appropriate length of time before yielding back to the caller. If a stopper 104 // is present, Next will eagerly return false when the stopper is stopped. 105 func (r *Retry) Next() bool { 106 if r.isReset { 107 r.isReset = false 108 return true 109 } 110 111 if r.opts.MaxRetries > 0 && r.currentAttempt >= r.opts.MaxRetries { 112 return false 113 } 114 115 // Wait before retry. 116 select { 117 case <-time.After(r.retryIn()): 118 r.currentAttempt++ 119 return true 120 case <-r.opts.Closer: 121 return false 122 case <-r.ctxDoneChan: 123 return false 124 } 125 } 126 127 // closedC is returned from Retry.NextCh whenever a retry 128 // can begin immediately. 129 var closedC = func() chan time.Time { 130 c := make(chan time.Time) 131 close(c) 132 return c 133 }() 134 135 // NextCh returns a channel which will receive when the next retry 136 // interval has expired. 137 func (r *Retry) NextCh() <-chan time.Time { 138 if r.isReset { 139 r.isReset = false 140 return closedC 141 } 142 r.currentAttempt++ 143 if r.opts.MaxRetries > 0 && r.currentAttempt > r.opts.MaxRetries { 144 return nil 145 } 146 return time.After(r.retryIn()) 147 } 148 149 // WithMaxAttempts is a helper that runs fn N times and collects the last err. 150 // It guarantees fn will run at least once. Otherwise, an error will be returned. 151 func WithMaxAttempts(ctx context.Context, opts Options, n int, fn func() error) error { 152 if n <= 0 { 153 return errors.Errorf("max attempts should not be 0 or below, got: %d", n) 154 } 155 156 opts.MaxRetries = n - 1 157 var err error 158 for r := StartWithCtx(ctx, opts); r.Next(); { 159 err = fn() 160 if err == nil { 161 return nil 162 } 163 } 164 if err == nil { 165 if ctx.Err() != nil { 166 err = errors.Wrap(ctx.Err(), "did not run function due to context completion") 167 } else { 168 err = errors.New("did not run function due to closed opts.Closer") 169 } 170 } 171 return err 172 } 173 174 // ForDuration will retry the given function until it either returns 175 // without error, or the given duration has elapsed. The function is invoked 176 // immediately at first and then successively with an exponential backoff 177 // starting at 1ns and ending at the specified duration. 178 // 179 // This function is DEPRECATED! Please use one of the other functions in this 180 // package that takes context cancellation into account. 181 // 182 // TODO(benesch): remove this function and port its callers to a context- 183 // sensitive API. 184 func ForDuration(duration time.Duration, fn func() error) error { 185 deadline := timeutil.Now().Add(duration) 186 var lastErr error 187 for wait := time.Duration(1); timeutil.Now().Before(deadline); wait *= 2 { 188 lastErr = fn() 189 if lastErr == nil { 190 return nil 191 } 192 if wait > time.Second { 193 wait = time.Second 194 } 195 time.Sleep(wait) 196 } 197 return lastErr 198 }