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  }