github.com/go-playground/pkg/v5@v5.29.1/errors/retrier_test.go (about) 1 //go:build go1.18 2 // +build go1.18 3 4 package errorsext 5 6 import ( 7 "context" 8 "errors" 9 "io" 10 "testing" 11 "time" 12 13 . "github.com/go-playground/assert/v2" 14 . "github.com/go-playground/pkg/v5/values/result" 15 ) 16 17 // TODO: Add IsRetryable and Retryable to helper functions. 18 19 func TestRetrierMaxAttempts(t *testing.T) { 20 var i, j int 21 result := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) { 22 j++ 23 }).MaxAttempts(MaxAttempts, 3).Do(context.Background(), func(ctx context.Context) Result[int, error] { 24 i++ 25 if i > 50 { 26 panic("infinite loop") 27 } 28 return Err[int, error](io.EOF) 29 }) 30 Equal(t, result.IsErr(), true) 31 Equal(t, result.Err(), io.EOF) 32 Equal(t, i, 3) 33 Equal(t, j, 2) 34 } 35 36 func TestRetrierMaxAttemptsNonRetryable(t *testing.T) { 37 var i, j int 38 returnErr := io.ErrUnexpectedEOF 39 result := NewRetryer[int, error]().IsRetryableFn(func(_ context.Context, e error) (isRetryable bool) { 40 if returnErr == io.EOF { 41 return false 42 } else { 43 return true 44 } 45 }).Backoff(func(ctx context.Context, attempt int, _ error) { 46 j++ 47 if j == 10 { 48 returnErr = io.EOF 49 } 50 }).MaxAttempts(MaxAttemptsNonRetryable, 3).Do(context.Background(), func(ctx context.Context) Result[int, error] { 51 i++ 52 if i > 50 { 53 panic("infinite loop") 54 } 55 return Err[int, error](returnErr) 56 }) 57 Equal(t, result.IsErr(), true) 58 Equal(t, result.Err(), io.EOF) 59 Equal(t, i, 13) 60 Equal(t, j, 12) 61 } 62 63 func TestRetrierMaxAttemptsNonRetryableReset(t *testing.T) { 64 var i, j int 65 returnErr := io.EOF 66 result := NewRetryer[int, error]().IsRetryableFn(func(_ context.Context, e error) (isRetryable bool) { 67 if returnErr == io.EOF { 68 return false 69 } else { 70 return true 71 } 72 }).Backoff(func(ctx context.Context, attempt int, _ error) { 73 j++ 74 if j == 2 { 75 returnErr = io.ErrUnexpectedEOF 76 } else if j == 10 { 77 returnErr = io.EOF 78 } 79 }).MaxAttempts(MaxAttemptsNonRetryableReset, 3).Do(context.Background(), func(ctx context.Context) Result[int, error] { 80 i++ 81 if i > 50 { 82 panic("infinite loop") 83 } 84 return Err[int, error](returnErr) 85 }) 86 Equal(t, result.IsErr(), true) 87 Equal(t, result.Err(), io.EOF) 88 Equal(t, i, 13) 89 Equal(t, j, 12) 90 } 91 92 func TestRetrierMaxAttemptsUnlimited(t *testing.T) { 93 var i, j int 94 r := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) { 95 j++ 96 }).MaxAttempts(MaxAttemptsUnlimited, 0) 97 98 PanicMatches(t, func() { 99 r.Do(context.Background(), func(ctx context.Context) Result[int, error] { 100 i++ 101 if i > 50 { 102 panic("infinite loop") 103 } 104 return Err[int, error](io.EOF) 105 }) 106 }, "infinite loop") 107 } 108 109 func TestRetrierMaxAttemptsTimeout(t *testing.T) { 110 result := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) { 111 }).MaxAttempts(MaxAttempts, 1).Timeout(time.Second). 112 Do(context.Background(), func(ctx context.Context) Result[int, error] { 113 select { 114 case <-ctx.Done(): 115 return Err[int, error](ctx.Err()) 116 case <-time.After(time.Second * 3): 117 return Err[int, error](io.EOF) 118 } 119 }) 120 Equal(t, result.IsErr(), true) 121 Equal(t, result.Err(), context.DeadlineExceeded) 122 } 123 124 func TestRetrierEarlyReturn(t *testing.T) { 125 var earlyReturnCount int 126 127 r := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) { 128 }).MaxAttempts(MaxAttempts, 5).Timeout(time.Second). 129 IsEarlyReturnFn(func(ctx context.Context, err error) bool { 130 earlyReturnCount++ 131 return errors.Is(err, io.EOF) 132 }).Backoff(nil) 133 134 result := r.Do(context.Background(), func(ctx context.Context) Result[int, error] { 135 return Err[int, error](io.EOF) 136 }) 137 Equal(t, result.IsErr(), true) 138 Equal(t, result.Err(), io.EOF) 139 Equal(t, earlyReturnCount, 1) 140 141 // now let try with retryable overriding early return TL;DR retryable should take precedence over early return 142 earlyReturnCount = 0 143 isRetryableCount := 0 144 result = r.IsRetryableFn(func(ctx context.Context, err error) (isRetryable bool) { 145 isRetryableCount++ 146 return errors.Is(err, io.EOF) 147 }).Do(context.Background(), func(ctx context.Context) Result[int, error] { 148 return Err[int, error](io.EOF) 149 }) 150 Equal(t, result.IsErr(), true) 151 Equal(t, result.Err(), io.EOF) 152 Equal(t, earlyReturnCount, 0) 153 Equal(t, isRetryableCount, 5) 154 155 // while here let's check the first test case again, `Retrier` should be a copy and original still intact. 156 isRetryableCount = 0 157 result = r.Do(context.Background(), func(ctx context.Context) Result[int, error] { 158 return Err[int, error](io.EOF) 159 }) 160 Equal(t, result.IsErr(), true) 161 Equal(t, result.Err(), io.EOF) 162 Equal(t, earlyReturnCount, 1) 163 Equal(t, isRetryableCount, 0) 164 }