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  }