github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/retry/retry_test.go (about) 1 // Copyright 2014 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package retry 12 13 import ( 14 "context" 15 "testing" 16 "time" 17 18 "github.com/cockroachdb/errors" 19 "github.com/stretchr/testify/require" 20 ) 21 22 func TestRetryExceedsMaxBackoff(t *testing.T) { 23 opts := Options{ 24 InitialBackoff: time.Microsecond * 10, 25 MaxBackoff: time.Microsecond * 100, 26 Multiplier: 2, 27 MaxRetries: 10, 28 } 29 30 r := Start(opts) 31 r.opts.RandomizationFactor = 0 32 for i := 0; i < 10; i++ { 33 d := r.retryIn() 34 if d > opts.MaxBackoff { 35 t.Fatalf("expected backoff less than max-backoff: %s vs %s", d, opts.MaxBackoff) 36 } 37 r.currentAttempt++ 38 } 39 } 40 41 func TestRetryExceedsMaxAttempts(t *testing.T) { 42 opts := Options{ 43 InitialBackoff: time.Microsecond * 10, 44 MaxBackoff: time.Second, 45 Multiplier: 2, 46 MaxRetries: 1, 47 } 48 49 attempts := 0 50 for r := Start(opts); r.Next(); attempts++ { 51 } 52 53 if expAttempts := opts.MaxRetries + 1; attempts != expAttempts { 54 t.Errorf("expected %d attempts, got %d attempts", expAttempts, attempts) 55 } 56 } 57 58 func TestRetryReset(t *testing.T) { 59 opts := Options{ 60 InitialBackoff: time.Microsecond * 10, 61 MaxBackoff: time.Second, 62 Multiplier: 2, 63 MaxRetries: 1, 64 } 65 66 expAttempts := opts.MaxRetries + 1 67 68 attempts := 0 69 // Backoff loop has 1 allowed retry; we always call Reset, so 70 // just make sure we get to 2 attempts and then break. 71 for r := Start(opts); r.Next(); attempts++ { 72 if attempts == expAttempts { 73 break 74 } 75 r.Reset() 76 } 77 if attempts != expAttempts { 78 t.Errorf("expected %d attempts, got %d", expAttempts, attempts) 79 } 80 } 81 82 func TestRetryStop(t *testing.T) { 83 closer := make(chan struct{}) 84 85 opts := Options{ 86 InitialBackoff: time.Second, 87 MaxBackoff: time.Second, 88 Multiplier: 2, 89 Closer: closer, 90 } 91 92 var attempts int 93 94 // Create a retry loop which will never stop without stopper. 95 for r := Start(opts); r.Next(); attempts++ { 96 go close(closer) 97 // Don't race the stopper, just wait for it to do its thing. 98 <-opts.Closer 99 } 100 101 if expAttempts := 1; attempts != expAttempts { 102 t.Errorf("expected %d attempts, got %d", expAttempts, attempts) 103 } 104 } 105 106 func TestRetryNextCh(t *testing.T) { 107 var attempts int 108 109 opts := Options{ 110 InitialBackoff: time.Millisecond, 111 Multiplier: 2, 112 MaxRetries: 1, 113 } 114 for r := Start(opts); attempts < 3; attempts++ { 115 c := r.NextCh() 116 if r.currentAttempt != attempts { 117 t.Errorf("expected attempt=%d; got %d", attempts, r.currentAttempt) 118 } 119 switch attempts { 120 case 0: 121 if c == nil { 122 t.Errorf("expected non-nil NextCh() on first attempt") 123 } 124 if _, ok := <-c; ok { 125 t.Errorf("expected closed (immediate) NextCh() on first attempt") 126 } 127 case 1: 128 if c == nil { 129 t.Errorf("expected non-nil NextCh() on second attempt") 130 } 131 if _, ok := <-c; !ok { 132 t.Errorf("expected open (delayed) NextCh() on first attempt") 133 } 134 case 2: 135 if c != nil { 136 t.Errorf("expected nil NextCh() on third attempt") 137 } 138 default: 139 t.Fatalf("unexpected attempt %d", attempts) 140 } 141 } 142 } 143 144 func TestRetryWithMaxAttempts(t *testing.T) { 145 expectedErr := errors.New("placeholder") 146 attempts := 0 147 errWithAttemptsCounterFunc := func() error { 148 attempts++ 149 return expectedErr 150 } 151 errorUntilAttemptNumFunc := func(until int) func() error { 152 return func() error { 153 attempts++ 154 if attempts == until { 155 return nil 156 } 157 return expectedErr 158 } 159 } 160 cancelCtx, cancelCtxFunc := context.WithCancel(context.Background()) 161 closeCh := make(chan struct{}) 162 163 testCases := []struct { 164 desc string 165 166 ctx context.Context 167 opts Options 168 preWithMaxAttemptsFunc func() 169 retryFunc func() error 170 maxAttempts int 171 172 // Due to channel races with select, we can allow a range of number of attempts. 173 minNumAttempts int 174 maxNumAttempts int 175 expectedErrText string 176 }{ 177 { 178 desc: "succeeds when no errors are ever given", 179 ctx: context.Background(), 180 opts: Options{ 181 InitialBackoff: time.Microsecond * 10, 182 MaxBackoff: time.Microsecond * 20, 183 Multiplier: 2, 184 MaxRetries: 1, 185 }, 186 retryFunc: func() error { return nil }, 187 maxAttempts: 3, 188 189 minNumAttempts: 0, 190 maxNumAttempts: 0, 191 }, 192 { 193 desc: "succeeds after one faked error", 194 ctx: context.Background(), 195 opts: Options{ 196 InitialBackoff: time.Microsecond * 10, 197 MaxBackoff: time.Microsecond * 20, 198 Multiplier: 2, 199 MaxRetries: 1, 200 }, 201 retryFunc: errorUntilAttemptNumFunc(1), 202 maxAttempts: 3, 203 204 minNumAttempts: 1, 205 maxNumAttempts: 1, 206 }, 207 { 208 desc: "errors when max attempts is exhausted", 209 ctx: context.Background(), 210 opts: Options{ 211 InitialBackoff: time.Microsecond * 10, 212 MaxBackoff: time.Microsecond * 20, 213 Multiplier: 2, 214 MaxRetries: 1, 215 }, 216 retryFunc: errWithAttemptsCounterFunc, 217 maxAttempts: 3, 218 219 minNumAttempts: 3, 220 maxNumAttempts: 3, 221 expectedErrText: expectedErr.Error(), 222 }, 223 { 224 desc: "errors with context that is canceled", 225 ctx: cancelCtx, 226 opts: Options{ 227 InitialBackoff: time.Microsecond * 10, 228 MaxBackoff: time.Microsecond * 20, 229 Multiplier: 2, 230 MaxRetries: 1, 231 }, 232 retryFunc: errWithAttemptsCounterFunc, 233 maxAttempts: 3, 234 preWithMaxAttemptsFunc: func() { 235 cancelCtxFunc() 236 }, 237 238 minNumAttempts: 0, 239 maxNumAttempts: 3, 240 expectedErrText: "did not run function due to context completion: context canceled", 241 }, 242 { 243 desc: "errors with opt.Closer that is closed", 244 ctx: context.Background(), 245 opts: Options{ 246 InitialBackoff: time.Microsecond * 10, 247 MaxBackoff: time.Microsecond * 20, 248 Multiplier: 2, 249 MaxRetries: 1, 250 Closer: closeCh, 251 }, 252 retryFunc: errWithAttemptsCounterFunc, 253 maxAttempts: 3, 254 preWithMaxAttemptsFunc: func() { 255 close(closeCh) 256 }, 257 258 minNumAttempts: 0, 259 maxNumAttempts: 3, 260 expectedErrText: "did not run function due to closed opts.Closer", 261 }, 262 } 263 264 for _, tc := range testCases { 265 t.Run(tc.desc, func(t *testing.T) { 266 attempts = 0 267 if tc.preWithMaxAttemptsFunc != nil { 268 tc.preWithMaxAttemptsFunc() 269 } 270 err := WithMaxAttempts(tc.ctx, tc.opts, tc.maxAttempts, tc.retryFunc) 271 if tc.expectedErrText != "" { 272 // Error can be either the expected error or the error timeout, as 273 // channels can race. 274 require.Truef( 275 t, 276 err.Error() == tc.expectedErrText || err.Error() == expectedErr.Error(), 277 "expected %s or %s, got %s", 278 tc.expectedErrText, 279 expectedErr.Error(), 280 err.Error(), 281 ) 282 } else { 283 require.NoError(t, err) 284 } 285 require.GreaterOrEqual(t, attempts, tc.minNumAttempts) 286 require.LessOrEqual(t, attempts, tc.maxNumAttempts) 287 }) 288 } 289 }