retrier.go 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. // Package retrier implements the "retriable" resiliency pattern for Go.
  2. package retrier
  3. import (
  4. "context"
  5. "math/rand"
  6. "sync"
  7. "time"
  8. )
  9. // Retrier implements the "retriable" resiliency pattern, abstracting out the process of retrying a failed action
  10. // a certain number of times with an optional back-off between each retry.
  11. type Retrier struct {
  12. backoff []time.Duration
  13. class Classifier
  14. jitter float64
  15. rand *rand.Rand
  16. randMu sync.Mutex
  17. }
  18. // New constructs a Retrier with the given backoff pattern and classifier. The length of the backoff pattern
  19. // indicates how many times an action will be retried, and the value at each index indicates the amount of time
  20. // waited before each subsequent retry. The classifier is used to determine which errors should be retried and
  21. // which should cause the retrier to fail fast. The DefaultClassifier is used if nil is passed.
  22. func New(backoff []time.Duration, class Classifier) *Retrier {
  23. if class == nil {
  24. class = DefaultClassifier{}
  25. }
  26. return &Retrier{
  27. backoff: backoff,
  28. class: class,
  29. rand: rand.New(rand.NewSource(time.Now().UnixNano())),
  30. }
  31. }
  32. // Run executes the given work function by executing RunCtx without context.Context.
  33. func (r *Retrier) Run(work func() error) error {
  34. return r.RunCtx(context.Background(), func(ctx context.Context) error {
  35. // never use ctx
  36. return work()
  37. })
  38. }
  39. // RunCtx executes the given work function, then classifies its return value based on the classifier used
  40. // to construct the Retrier. If the result is Succeed or Fail, the return value of the work function is
  41. // returned to the caller. If the result is Retry, then Run sleeps according to the its backoff policy
  42. // before retrying. If the total number of retries is exceeded then the return value of the work function
  43. // is returned to the caller regardless.
  44. func (r *Retrier) RunCtx(ctx context.Context, work func(ctx context.Context) error) error {
  45. retries := 0
  46. for {
  47. ret := work(ctx)
  48. switch r.class.Classify(ret) {
  49. case Succeed, Fail:
  50. return ret
  51. case Retry:
  52. if retries >= len(r.backoff) {
  53. return ret
  54. }
  55. timeout := time.After(r.calcSleep(retries))
  56. if err := r.sleep(ctx, timeout); err != nil {
  57. return err
  58. }
  59. retries++
  60. }
  61. }
  62. }
  63. func (r *Retrier) sleep(ctx context.Context, t <-chan time.Time) error {
  64. select {
  65. case <-t:
  66. return nil
  67. case <-ctx.Done():
  68. return ctx.Err()
  69. }
  70. }
  71. func (r *Retrier) calcSleep(i int) time.Duration {
  72. // lock unsafe rand prng
  73. r.randMu.Lock()
  74. defer r.randMu.Unlock()
  75. // take a random float in the range (-r.jitter, +r.jitter) and multiply it by the base amount
  76. return r.backoff[i] + time.Duration(((r.rand.Float64()*2)-1)*r.jitter*float64(r.backoff[i]))
  77. }
  78. // SetJitter sets the amount of jitter on each back-off to a factor between 0.0 and 1.0 (values outside this range
  79. // are silently ignored). When a retry occurs, the back-off is adjusted by a random amount up to this value.
  80. func (r *Retrier) SetJitter(jit float64) {
  81. if jit < 0 || jit > 1 {
  82. return
  83. }
  84. r.jitter = jit
  85. }