github.com/influx6/npkg@v0.8.8/nretries/retries.go (about)

     1  package nretries
     2  
     3  import (
     4  	"math"
     5  	"math/rand"
     6  	"time"
     7  )
     8  
     9  // LinearDoUntil will continuously run the giving function until no error is returned.
    10  // If duration is supplied, the goroutine is made to sleep before making next run.
    11  // The same duration is consistently used for each sleep.
    12  // The last error received is returned if the count is exhausted without formally
    13  // ending function call without an error.
    14  func LinearDoUntil(fx func() error, total int, elapse time.Duration) error {
    15  	var err error
    16  	for i := total; i > 0; i-- {
    17  		if err = fx(); err == nil {
    18  			return nil
    19  		}
    20  
    21  		if elapse > 0 {
    22  			time.Sleep(elapse)
    23  		}
    24  	}
    25  	return err
    26  }
    27  
    28  //***************************************************************
    29  // BackOff Generators
    30  //
    31  // Taken from the ff:
    32  // 1. https://github.com/sethgrid/pester
    33  // 2. https://github.com/cenkalti/backoff
    34  // 3. https://github.com/hashicorp/go-retryablehttp
    35  //***************************************************************
    36  
    37  var (
    38  	// random is used to generate pseudo-random numbers.
    39  	random = rand.New(rand.NewSource(time.Now().UnixNano()))
    40  )
    41  
    42  // LinearBackOff returns increasing durations, each a second longer than the last
    43  func LinearBackOff(i int) time.Duration {
    44  	return time.Duration(i) * time.Second
    45  }
    46  
    47  // ExponentialBackOff returns ever increasing backoffs by a power of 2
    48  func ExponentialBackOff(i int) time.Duration {
    49  	return time.Duration(1<<uint(i)) * time.Second
    50  }
    51  
    52  // ExponentialJitterBackOff returns ever increasing backoffs by a power of 2
    53  // with +/- 0-33% to prevent synchronized requests.
    54  func ExponentialJitterBackOff(i int) time.Duration {
    55  	return JitterDuration(int(1 << uint(i)))
    56  }
    57  
    58  // LinearJitterBackOff returns increasing durations, each a second longer than the last
    59  // with +/- 0-33% to prevent synchronized requests.
    60  func LinearJitterBackOff(i int) time.Duration {
    61  	return JitterDuration(i)
    62  }
    63  
    64  // JitterDuration keeps the +/- 0-33% logic in one place
    65  func JitterDuration(i int) time.Duration {
    66  	ms := i * 1000
    67  	maxJitter := ms / 3
    68  
    69  	// ms ± rand
    70  	ms += random.Intn(2*maxJitter) - maxJitter
    71  
    72  	// a jitter of 0 messes up the time.Tick chan
    73  	if ms <= 0 {
    74  		ms = 1
    75  	}
    76  
    77  	return time.Duration(ms) * time.Millisecond
    78  }
    79  
    80  // RandomizedJitters returns a function which returns a new duration for randomized
    81  // value of passed integer ranged within provided initial time duration seed.
    82  func RandomizedJitters(initial time.Duration, randomFactor float64) func(int) time.Duration {
    83  	return func(attempt int) time.Duration {
    84  		rr := math.Abs(random.Float64())
    85  		initial = GetRandomValueFromInterval(randomFactor, rr*float64(attempt), initial)
    86  		return initial
    87  	}
    88  }
    89  
    90  // LinearRangeJitters returns a function which returns a new duration for increasing
    91  // value of passed integer ranged within provided minimum and maximum time durations.
    92  func LinearRangeJitters(min, max time.Duration) func(int) time.Duration {
    93  	return func(attempt int) time.Duration {
    94  		return LinearRangedJitterBackOff(min, max, attempt)
    95  	}
    96  }
    97  
    98  // RangedExponential returns a function which returns a new duration for increasing
    99  // value of passed integer ranged within provided minimum and maximum time durations.
   100  func RangedExponential(min, max time.Duration) func(int) time.Duration {
   101  	return func(attempt int) time.Duration {
   102  		return RangeExponentialBackOff(min, max, attempt)
   103  	}
   104  }
   105  
   106  // RangeExponentialBackOff provides a back off value which will perform
   107  // exponential back off based on the attempt number and limited
   108  // by the provided minimum and maximum durations.
   109  func RangeExponentialBackOff(min, max time.Duration, attemptNum int) time.Duration {
   110  	mult := math.Pow(2, float64(attemptNum)) * float64(min)
   111  	sleep := time.Duration(mult)
   112  	if float64(sleep) != mult || sleep > max {
   113  		sleep = max
   114  	}
   115  	return sleep
   116  }
   117  
   118  // LinearRangedJitterBackOff provides a back off value which will
   119  // perform linear back off based on the attempt number and with jitter to
   120  // prevent a thundering herd.
   121  //
   122  // min and max here are *not* absolute values. The number to be multiplied by
   123  // the attempt number will be chosen at random from between them, thus they are
   124  // bounding the jitter.
   125  //
   126  // For instance:
   127  // * To get strictly linear back off of one second increasing each retry, set
   128  // both to one second (1s, 2s, 3s, 4s, ...)
   129  // * To get a small amount of jitter centered around one second increasing each
   130  // retry, set to around one second, such as a min of 800ms and max of 1200ms
   131  // (892ms, 2102ms, 2945ms, 4312ms, ...)
   132  // * To get extreme jitter, set to a very wide spread, such as a min of 100ms
   133  // and a max of 20s (15382ms, 292ms, 51321ms, 35234ms, ...)
   134  func LinearRangedJitterBackOff(min, max time.Duration, attemptNum int) time.Duration {
   135  	// attemptNum always starts at zero but we want to start at 1 for multiplication
   136  	attemptNum++
   137  
   138  	if max <= min {
   139  		// Unclear what to do here, or they are the same, so return min *
   140  		// attemptNum
   141  		return min * time.Duration(attemptNum)
   142  	}
   143  
   144  	// Pick a random number that lies somewhere between the min and max and
   145  	// multiply by the attemptNum. attemptNum starts at zero so we always
   146  	// increment here. We first get a random percentage, then apply that to the
   147  	// difference between min and max, and add to min.
   148  	jitter := random.Float64() * float64(max-min)
   149  	jitterMin := int64(jitter) + int64(min)
   150  	return time.Duration(jitterMin * int64(attemptNum))
   151  }
   152  
   153  // GetRandomValueFromInterval returns a random value from the following interval:
   154  // 	[randomizationFactor * currentInterval, randomizationFactor * currentInterval].
   155  func GetRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration {
   156  	var delta = randomizationFactor * float64(currentInterval)
   157  	var minInterval = float64(currentInterval) - delta
   158  	var maxInterval = float64(currentInterval) + delta
   159  
   160  	// Get a random value from the range [minInterval, maxInterval].
   161  	// The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
   162  	// we want a 33% chance for selecting either 1, 2 or 3.
   163  	return time.Duration(minInterval + (random * (maxInterval - minInterval + 1)))
   164  }