...

Source file src/github.com/launchdarkly/eventsource/retry_delay.go

Documentation: github.com/launchdarkly/eventsource

     1  package eventsource
     2  
     3  import (
     4  	"math"
     5  	"math/rand"
     6  	"time"
     7  )
     8  
     9  // Encapsulation of configurable backoff/jitter behavior.
    10  //
    11  // - The system can either be in a "good" state or a "bad" state. The initial state is "bad"; the
    12  // caller is responsible for indicating when it transitions to "good". When we ask for a new retry
    13  // delay, that implies the state is now transitioning to "bad".
    14  //
    15  // - There is a configurable base delay, which can be changed at any time (if the SSE server sends
    16  // us a "retry:" directive).
    17  //
    18  // - There are optional strategies for applying backoff and jitter to the delay.
    19  //
    20  // This object is meant to be used from a single goroutine once it's been created; its methods are
    21  // not safe for concurrent use.
    22  type retryDelayStrategy struct {
    23  	baseDelay     time.Duration
    24  	backoff       backoffStrategy
    25  	jitter        jitterStrategy
    26  	resetInterval time.Duration
    27  	retryCount    int
    28  	goodSince     time.Time // nonzero only if the state is currently "good"
    29  }
    30  
    31  // Abstraction for backoff delay behavior.
    32  type backoffStrategy interface {
    33  	applyBackoff(baseDelay time.Duration, retryCount int) time.Duration
    34  }
    35  
    36  // Abstraction for delay jitter behavior.
    37  type jitterStrategy interface {
    38  	applyJitter(computedDelay time.Duration) time.Duration
    39  }
    40  
    41  type defaultBackoffStrategy struct {
    42  	maxDelay time.Duration
    43  }
    44  
    45  // Creates the default implementation of exponential backoff, which doubles the delay each time up to
    46  // the specified maximum.
    47  //
    48  // If a resetInterval was specified for the retryDelayStrategy, and the system has been in a "good"
    49  // state for at least that long, the delay is reset back to the base. This avoids perpetually increasing
    50  // delays in a situation where failures are rare).
    51  func newDefaultBackoff(maxDelay time.Duration) backoffStrategy {
    52  	return defaultBackoffStrategy{maxDelay}
    53  }
    54  
    55  func (s defaultBackoffStrategy) applyBackoff(baseDelay time.Duration, retryCount int) time.Duration {
    56  	d := math.Min(float64(baseDelay)*math.Pow(2, float64(retryCount)), float64(s.maxDelay))
    57  	return time.Duration(d)
    58  }
    59  
    60  type defaultJitterStrategy struct {
    61  	ratio  float64
    62  	random *rand.Rand
    63  }
    64  
    65  // Creates the default implementation of jitter, which subtracts a pseudo-random amount from each delay.
    66  // The ratio parameter should be greater than 0 and less than or equal to 1.0.
    67  func newDefaultJitter(ratio float64, randSeed int64) jitterStrategy {
    68  	if randSeed <= 0 {
    69  		randSeed = time.Now().UnixNano()
    70  	}
    71  	if ratio > 1.0 {
    72  		ratio = 1.0
    73  	}
    74  	return &defaultJitterStrategy{ratio, rand.New(rand.NewSource(randSeed))}
    75  }
    76  
    77  func (s *defaultJitterStrategy) applyJitter(computedDelay time.Duration) time.Duration {
    78  	// retryCount doesn't matter here - it's included in the int
    79  	jitter := time.Duration(s.random.Int63n(int64(float64(computedDelay) * s.ratio)))
    80  	return computedDelay - jitter
    81  }
    82  
    83  // Creates a retryDelayStrategy.
    84  func newRetryDelayStrategy(
    85  	baseDelay time.Duration,
    86  	resetInterval time.Duration,
    87  	backoff backoffStrategy,
    88  	jitter jitterStrategy,
    89  ) *retryDelayStrategy {
    90  	return &retryDelayStrategy{
    91  		baseDelay:     baseDelay,
    92  		resetInterval: resetInterval,
    93  		backoff:       backoff,
    94  		jitter:        jitter,
    95  	}
    96  }
    97  
    98  // NextRetryDelay computes the next retry interval. This also sets the current state to "bad".
    99  //
   100  // Note that currentTime is passed as a parameter instead of computed by this function to guarantee predictable
   101  // behavior in tests.
   102  func (r *retryDelayStrategy) NextRetryDelay(currentTime time.Time) time.Duration {
   103  	if !r.goodSince.IsZero() && r.resetInterval > 0 && (currentTime.Sub(r.goodSince) >= r.resetInterval) {
   104  		r.retryCount = 0
   105  	}
   106  	r.goodSince = time.Time{}
   107  	delay := r.baseDelay
   108  	if r.backoff != nil {
   109  		delay = r.backoff.applyBackoff(delay, r.retryCount)
   110  	}
   111  	r.retryCount++
   112  	if r.jitter != nil {
   113  		delay = r.jitter.applyJitter(delay)
   114  	}
   115  	return delay
   116  }
   117  
   118  // SetGoodSince marks the current state as "good" and records the time. See comments on the backoff type.
   119  func (r *retryDelayStrategy) SetGoodSince(goodSince time.Time) {
   120  	r.goodSince = goodSince
   121  }
   122  
   123  // SetBaseDelay changes the initial retry delay and resets the backoff (if any) so the next retry will use
   124  // that value.
   125  //
   126  // This is used to implement the optional SSE behavior where the server sends a "retry:" command to
   127  // set the base retry to a specific value. Note that we will still apply a jitter, if jitter is enabled,
   128  // and subsequent retries will still increase exponentially.
   129  func (r *retryDelayStrategy) SetBaseDelay(baseDelay time.Duration) {
   130  	r.baseDelay = baseDelay
   131  	r.retryCount = 0
   132  }
   133  
   134  func (r *retryDelayStrategy) hasJitter() bool { //nolint:megacheck // used only in tests
   135  	return r.jitter != nil
   136  }
   137  

View as plain text