go.uber.org/yarpc@v1.72.1/internal/backoff/exponential_test.go (about) 1 // Copyright (c) 2022 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 backoff 22 23 import ( 24 "math/rand" 25 "testing" 26 "time" 27 28 "github.com/stretchr/testify/assert" 29 ) 30 31 func TestInvalidFirst(t *testing.T) { 32 _, err := NewExponential( 33 FirstBackoff(time.Duration(0)), 34 ) 35 assert.Equal(t, err.Error(), "invalid first duration for exponential backoff, need greater than zero") 36 } 37 38 func TestInvalidMax(t *testing.T) { 39 _, err := NewExponential( 40 MaxBackoff(-1 * time.Second), 41 ) 42 assert.Equal(t, err.Error(), "invalid max for exponential backoff, need greater than or equal to zero") 43 } 44 45 func TestInvalidFirstAndMax(t *testing.T) { 46 _, err := NewExponential( 47 FirstBackoff(time.Duration(0)), 48 MaxBackoff(-1*time.Second), 49 ) 50 assert.Equal(t, err.Error(), "invalid first duration for exponential backoff, need greater than zero; invalid max for exponential backoff, need greater than or equal to zero") 51 } 52 53 func TestExponential(t *testing.T) { 54 type backoffAttempt struct { 55 msg string 56 giveAttempt uint 57 giveRandResult int64 58 wantBackoff time.Duration 59 } 60 type testStruct struct { 61 msg string 62 63 giveFirst time.Duration 64 giveMax time.Duration 65 66 attempts []backoffAttempt 67 } 68 tests := []testStruct{ 69 { 70 msg: "valid durations", 71 giveFirst: time.Nanosecond, 72 giveMax: time.Nanosecond * 100, 73 attempts: []backoffAttempt{ 74 { 75 msg: "zero attempt max backoff", 76 giveAttempt: 0, 77 giveRandResult: int64(1 << 0), 78 wantBackoff: time.Nanosecond, 79 }, 80 { 81 msg: "zero attempt min backoff", 82 giveAttempt: 0, 83 giveRandResult: 0, 84 wantBackoff: time.Duration(0), 85 }, 86 { 87 msg: "zero attempt min backoff (with wrapped rand value)", 88 giveAttempt: 0, 89 giveRandResult: int64(1<<0) + 1, 90 wantBackoff: time.Duration(0), 91 }, 92 { 93 msg: "one attempt max backoff", 94 giveAttempt: 1, 95 giveRandResult: int64(1 << 1), 96 wantBackoff: time.Nanosecond * 2, 97 }, 98 { 99 msg: "two attempts max backoff", 100 giveAttempt: 2, 101 giveRandResult: int64(1 << 2), 102 wantBackoff: time.Duration(int64(1 << 2)), 103 }, 104 { 105 msg: "three attempts max backoff", 106 giveAttempt: 3, 107 giveRandResult: int64(1 << 3), 108 wantBackoff: time.Duration(int64(1 << 3)), 109 }, 110 { 111 msg: "four attempts max backoff", 112 giveAttempt: 4, 113 giveRandResult: int64(1 << 4), 114 wantBackoff: time.Duration(int64(1 << 4)), 115 }, 116 { 117 msg: "four attempts min backoff (with wrapped rand value)", 118 giveAttempt: 4, 119 giveRandResult: int64(1<<4) + 1, 120 wantBackoff: time.Duration(0), 121 }, 122 { 123 msg: "attempts range higher than max value", 124 giveAttempt: 30, 125 giveRandResult: 100, 126 wantBackoff: time.Nanosecond * 100, 127 }, 128 { 129 msg: "attempts range higher than max value (with wrapped rand value)", 130 giveAttempt: 30, 131 giveRandResult: 100 + 1, 132 wantBackoff: time.Duration(0), 133 }, 134 { 135 msg: "attempts that cause overflows should go to max", 136 giveAttempt: 63, // 1<<63 == -9223372036854775808 137 giveRandResult: 100, 138 wantBackoff: time.Nanosecond * 100, 139 }, 140 { 141 msg: "attempts that cause overflows should go to max (with wrapped rand)", 142 giveAttempt: 63, // 1<<63 == -9223372036854775808 143 giveRandResult: 100 + 1, 144 wantBackoff: time.Duration(0), 145 }, 146 { 147 msg: "attempts that go beyond overflows should go to max", 148 giveAttempt: 64, // 1<<64 == 0 149 giveRandResult: 100, 150 wantBackoff: time.Nanosecond * 100, 151 }, 152 { 153 msg: "attempts that go beyond overflows should go to max (with wrapped rand)", 154 giveAttempt: 64, // 1<<64 == 0 155 giveRandResult: 100 + 1, 156 wantBackoff: time.Duration(0), 157 }, 158 { 159 msg: "max value with a random value that i choose", 160 giveAttempt: 14, 161 giveRandResult: 68, 162 wantBackoff: time.Duration(68), 163 }, 164 }, 165 }, 166 } 167 168 for _, tt := range tests { 169 t.Run(tt.msg, func(t *testing.T) { 170 randSrc := &mutableRandSrc{val: 0} 171 strategy, err := NewExponential( 172 FirstBackoff(tt.giveFirst), 173 MaxBackoff(tt.giveMax), 174 randGenerator(func() *rand.Rand { return rand.New(randSrc) }), 175 ) 176 assert.NoError(t, err) 177 backoff := strategy.Backoff() 178 for _, attempt := range tt.attempts { 179 randSrc.val = attempt.giveRandResult 180 assert.Equal(t, attempt.wantBackoff, backoff.Duration(attempt.giveAttempt), "backoff for backoffAttempt %q did not match", attempt.msg) 181 } 182 }) 183 } 184 } 185 186 // mutableRandSrc implements the rand.Source interface so we can get our random 187 // number generator to return whatever we want. 188 type mutableRandSrc struct { 189 val int64 190 } 191 192 func (r *mutableRandSrc) Int63() int64 { 193 return r.val 194 } 195 196 func (*mutableRandSrc) Seed(int64) {}