github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/x/retry/retry_test.go (about) 1 // Copyright (c) 2016 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package retry 22 23 import ( 24 "errors" 25 "fmt" 26 "math" 27 "math/rand" 28 "os" 29 "testing" 30 "time" 31 32 "github.com/leanovate/gopter" 33 "github.com/leanovate/gopter/gen" 34 "github.com/leanovate/gopter/prop" 35 "github.com/stretchr/testify/assert" 36 ) 37 38 var ( 39 errTestFn = RetryableError(errors.New("an error")) 40 ) 41 42 type testFnOpts struct { 43 succeedAfter *int 44 errs []error 45 } 46 47 func newTestFn(opts testFnOpts) Fn { 48 return func() error { 49 if opts.succeedAfter != nil { 50 if *opts.succeedAfter == 0 { 51 return nil 52 } 53 *opts.succeedAfter-- 54 } 55 if len(opts.errs) > 0 { 56 err := opts.errs[0] 57 opts.errs = opts.errs[1:] 58 return err 59 } 60 return errTestFn 61 } 62 } 63 64 func testOptions() Options { 65 return NewOptions(). 66 SetInitialBackoff(time.Second). 67 SetBackoffFactor(2). 68 SetMaxRetries(2). 69 SetForever(false). 70 SetJitter(false) 71 } 72 73 func TestRetrierExponentialBackOffSuccess(t *testing.T) { 74 succeedAfter := 0 75 slept := time.Duration(0) 76 r := NewRetrier(testOptions()).(*retrier) 77 r.sleepFn = func(t time.Duration) { 78 slept += t 79 } 80 err := r.Attempt(newTestFn(testFnOpts{succeedAfter: &succeedAfter})) 81 assert.Nil(t, err) 82 assert.Equal(t, time.Duration(0), slept) 83 } 84 85 func TestRetrierExponentialBackOffSomeFailure(t *testing.T) { 86 succeedAfter := 2 87 slept := time.Duration(0) 88 r := NewRetrier(testOptions()).(*retrier) 89 r.sleepFn = func(t time.Duration) { 90 slept += t 91 } 92 err := r.Attempt(newTestFn(testFnOpts{succeedAfter: &succeedAfter})) 93 assert.Nil(t, err) 94 assert.Equal(t, 3*time.Second, slept) 95 } 96 97 func TestRetrierExponentialBackOffFailure(t *testing.T) { 98 slept := time.Duration(0) 99 r := NewRetrier(testOptions()).(*retrier) 100 r.sleepFn = func(t time.Duration) { 101 slept += t 102 } 103 err := r.Attempt(newTestFn(testFnOpts{})) 104 assert.Equal(t, errTestFn, err) 105 assert.Equal(t, 3*time.Second, slept) 106 } 107 108 func TestRetrierMaxBackoff(t *testing.T) { 109 succeedAfter := 3 110 opts := testOptions(). 111 SetMaxRetries(succeedAfter). 112 SetMaxBackoff(3 * time.Second) 113 slept := time.Duration(0) 114 r := NewRetrier(opts).(*retrier) 115 r.sleepFn = func(t time.Duration) { 116 slept += t 117 } 118 err := r.Attempt(newTestFn(testFnOpts{succeedAfter: &succeedAfter})) 119 assert.Nil(t, err) 120 assert.Equal(t, 6*time.Second, slept) 121 } 122 123 func TestRetrierExponentialBackOffBreakWhileImmediate(t *testing.T) { 124 slept := time.Duration(0) 125 r := NewRetrier(testOptions()).(*retrier) 126 r.sleepFn = func(t time.Duration) { 127 slept += t 128 } 129 err := r.AttemptWhile(func(_ int) bool { return false }, newTestFn(testFnOpts{})) 130 assert.Equal(t, ErrWhileConditionFalse, err) 131 assert.Equal(t, time.Duration(0), slept) 132 } 133 134 func TestRetrierExponentialBackOffBreakWhileSecondAttempt(t *testing.T) { 135 slept := time.Duration(0) 136 r := NewRetrier(testOptions()).(*retrier) 137 r.sleepFn = func(t time.Duration) { 138 slept += t 139 } 140 err := r.AttemptWhile(func(attempt int) bool { return attempt == 0 }, newTestFn(testFnOpts{})) 141 assert.Equal(t, ErrWhileConditionFalse, err) 142 assert.Equal(t, time.Second, slept) 143 } 144 145 func TestRetrierExponentialBackOffJitter(t *testing.T) { 146 succeedAfter := 1 147 slept := time.Duration(0) 148 r := NewRetrier(testOptions().SetJitter(true)).(*retrier) 149 r.sleepFn = func(t time.Duration) { 150 slept += t 151 } 152 err := r.Attempt(newTestFn(testFnOpts{succeedAfter: &succeedAfter})) 153 assert.Nil(t, err) 154 // Test slept < time.Second as rand.Float64 range is [0.0, 1.0) and 155 // also proves jitter is definitely applied 156 assert.True(t, 500*time.Millisecond <= slept && slept < time.Second) 157 } 158 159 func TestRetrierExponentialBackOffNonRetryableErrorImmediate(t *testing.T) { 160 slept := time.Duration(0) 161 r := NewRetrier(testOptions()).(*retrier) 162 r.sleepFn = func(t time.Duration) { 163 slept += t 164 } 165 expectedErr := NonRetryableError(fmt.Errorf("an error")) 166 err := r.Attempt(newTestFn(testFnOpts{errs: []error{expectedErr}})) 167 assert.Equal(t, expectedErr, err) 168 assert.Equal(t, time.Duration(0), slept) 169 } 170 171 func TestRetrierExponentialBackOffNonRetryableErrorSecondAttempt(t *testing.T) { 172 slept := time.Duration(0) 173 r := NewRetrier(testOptions()).(*retrier) 174 r.sleepFn = func(t time.Duration) { 175 slept += t 176 } 177 expectedErr := NonRetryableError(fmt.Errorf("an error")) 178 err := r.Attempt(newTestFn(testFnOpts{errs: []error{errTestFn, expectedErr}})) 179 assert.Equal(t, expectedErr, err) 180 assert.Equal(t, time.Second, slept) 181 } 182 183 func TestRetryForever(t *testing.T) { 184 var ( 185 errForever = errors.New("error forever") 186 numAttempts int 187 totalSlept time.Duration 188 ) 189 r := NewRetrier(testOptions().SetForever(true)).(*retrier) 190 r.sleepFn = func(t time.Duration) { 191 totalSlept += t 192 numAttempts++ 193 } 194 foreverFn := func() error { return errForever } 195 continueFn := func(attempt int) bool { return attempt < 10 } 196 197 err := r.AttemptWhile(continueFn, foreverFn) 198 assert.Equal(t, ErrWhileConditionFalse, err) 199 assert.Equal(t, 10, numAttempts) 200 assert.Equal(t, time.Duration(1023*time.Second), totalSlept) 201 } 202 203 func TestBackoffValidResult(t *testing.T) { 204 seed := time.Now().UnixNano() 205 parameters := gopter.DefaultTestParameters() 206 parameters.Rng = rand.New(rand.NewSource(seed)) 207 parameters.MinSuccessfulTests = 10000 208 props := gopter.NewProperties(parameters) 209 210 props.Property("Valid result", prop.ForAll( 211 func(retry int, jitter bool, backoffFactor float64, initialBackoff, maxBackoff int64) bool { 212 return BackoffNanos( 213 retry, 214 jitter, 215 backoffFactor, 216 time.Duration(initialBackoff), 217 time.Duration(maxBackoff), 218 rand.Int63n, 219 ) >= 0 220 }, 221 gen.IntRange(-100, 1000), 222 gen.Bool(), 223 gen.Float64Range(0, 1000), 224 gen.Int64Range(0, math.MaxInt64), 225 gen.Int64Range(0, math.MaxInt64), 226 )) 227 reporter := gopter.NewFormatedReporter(true, 160, os.Stdout) 228 if !props.Run(reporter) { 229 t.Errorf("failed with initial seed: %d", seed) 230 } 231 }