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  }