retry_test.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. package retry
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "time"
  7. )
  8. func TestRunSucceedsOnFirstTry(t *testing.T) {
  9. calls := 0
  10. res := Run(context.Background(), Default(), func(ctx context.Context, attempt int) error {
  11. calls++
  12. return nil
  13. })
  14. if calls != 1 {
  15. t.Fatalf("want 1 call, got %d", calls)
  16. }
  17. if res.Attempts != 1 {
  18. t.Fatalf("want Attempts=1, got %d", res.Attempts)
  19. }
  20. if res.LastError != nil {
  21. t.Fatalf("want nil err, got %v", res.LastError)
  22. }
  23. }
  24. func TestRunRetriesThenSucceeds(t *testing.T) {
  25. calls := 0
  26. cfg := Config{
  27. MaxAttempts: 5,
  28. BaseDelay: 1 * time.Millisecond,
  29. MaxDelay: 5 * time.Millisecond,
  30. Budget: 5 * time.Second,
  31. }
  32. res := Run(context.Background(), cfg, func(ctx context.Context, attempt int) error {
  33. calls++
  34. if attempt < 3 {
  35. return errors.New("transient")
  36. }
  37. return nil
  38. })
  39. if calls != 3 {
  40. t.Fatalf("want 3 calls, got %d", calls)
  41. }
  42. if res.Attempts != 3 {
  43. t.Fatalf("want Attempts=3, got %d", res.Attempts)
  44. }
  45. if res.LastError != nil {
  46. t.Fatalf("want nil err, got %v", res.LastError)
  47. }
  48. }
  49. func TestRunExhaustsAttempts(t *testing.T) {
  50. calls := 0
  51. cfg := Config{
  52. MaxAttempts: 3,
  53. BaseDelay: 1 * time.Millisecond,
  54. MaxDelay: 5 * time.Millisecond,
  55. Budget: 5 * time.Second,
  56. }
  57. res := Run(context.Background(), cfg, func(ctx context.Context, attempt int) error {
  58. calls++
  59. return errors.New("nope")
  60. })
  61. if calls != 3 {
  62. t.Fatalf("want 3 calls, got %d", calls)
  63. }
  64. if res.Attempts != 3 {
  65. t.Fatalf("want Attempts=3, got %d", res.Attempts)
  66. }
  67. if res.LastError == nil {
  68. t.Fatalf("want non-nil err")
  69. }
  70. }
  71. func TestRunShortCircuitsOnPermanent(t *testing.T) {
  72. calls := 0
  73. cfg := Config{
  74. MaxAttempts: 5,
  75. BaseDelay: 1 * time.Millisecond,
  76. MaxDelay: 5 * time.Millisecond,
  77. Budget: 5 * time.Second,
  78. }
  79. res := Run(context.Background(), cfg, func(ctx context.Context, attempt int) error {
  80. calls++
  81. return &PermanentError{Err: errors.New("bad token")}
  82. })
  83. if calls != 1 {
  84. t.Fatalf("want 1 call, got %d", calls)
  85. }
  86. if !IsPermanent(res.LastError) {
  87. t.Fatalf("want PermanentError, got %T: %v", res.LastError, res.LastError)
  88. }
  89. }
  90. func TestRunRespectsBudget(t *testing.T) {
  91. calls := 0
  92. cfg := Config{
  93. MaxAttempts: 100,
  94. BaseDelay: 50 * time.Millisecond,
  95. MaxDelay: 200 * time.Millisecond,
  96. Budget: 100 * time.Millisecond, // tight budget
  97. }
  98. start := time.Now()
  99. res := Run(context.Background(), cfg, func(ctx context.Context, attempt int) error {
  100. calls++
  101. return errors.New("nope")
  102. })
  103. elapsed := time.Since(start)
  104. if elapsed > 500*time.Millisecond {
  105. t.Fatalf("budget exceeded: took %s", elapsed)
  106. }
  107. // With Budget=100ms and base 50ms, we expect at most
  108. // 3-4 calls (50 + 100 = 150ms would already bust the
  109. // budget for the next wait).
  110. if calls > 5 {
  111. t.Fatalf("too many calls under tight budget: %d", calls)
  112. }
  113. if res.LastError == nil {
  114. t.Fatalf("want non-nil err")
  115. }
  116. }
  117. func TestRunRespectsContextCancel(t *testing.T) {
  118. ctx, cancel := context.WithCancel(context.Background())
  119. calls := 0
  120. cfg := Config{
  121. MaxAttempts: 10,
  122. BaseDelay: 100 * time.Millisecond,
  123. MaxDelay: 500 * time.Millisecond,
  124. Budget: 5 * time.Second,
  125. }
  126. // Cancel the ctx after the first call.
  127. res := Run(ctx, cfg, func(ctx context.Context, attempt int) error {
  128. calls++
  129. if calls == 1 {
  130. // Cancel while we're sleeping.
  131. go func() {
  132. time.Sleep(20 * time.Millisecond)
  133. cancel()
  134. }()
  135. }
  136. return errors.New("nope")
  137. })
  138. if calls >= 10 {
  139. t.Fatalf("ctx cancel did not stop the loop; calls=%d", calls)
  140. }
  141. if res.LastError == nil {
  142. t.Fatalf("want non-nil err")
  143. }
  144. }
  145. func TestBackoffMonotonic(t *testing.T) {
  146. // Per-attempt wait is the time we sleep BEFORE that
  147. // attempt. So attempt 1 = no wait, attempt 2 = base,
  148. // attempt 3 = base*2, etc., capped at max.
  149. base := 100 * time.Millisecond
  150. max := 2 * time.Second
  151. prev := time.Duration(0)
  152. for n := 2; n <= 10; n++ {
  153. got := backoff(base, max, n)
  154. if n == 2 && got != base {
  155. t.Fatalf("attempt 2: want %s, got %s", base, got)
  156. }
  157. if got > max {
  158. t.Fatalf("attempt %d: exceeded max: %s", n, got)
  159. }
  160. // Wait must be >= previous (or equal under cap).
  161. if got < prev {
  162. t.Fatalf("attempt %d: wait decreased: %s < %s", n, got, prev)
  163. }
  164. prev = got
  165. }
  166. }