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) {}