go.temporal.io/server@v1.23.0/common/backoff/retry_test.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package backoff
    26  
    27  import (
    28  	"context"
    29  	"fmt"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/stretchr/testify/require"
    34  	"github.com/stretchr/testify/suite"
    35  	"go.temporal.io/api/serviceerror"
    36  )
    37  
    38  type (
    39  	RetrySuite struct {
    40  		*require.Assertions // override suite.Suite.Assertions with require.Assertions; this means that s.NotNil(nil) will stop the test, not merely log an error
    41  		suite.Suite
    42  	}
    43  
    44  	someError struct{}
    45  )
    46  
    47  func TestRetrySuite(t *testing.T) {
    48  	suite.Run(t, new(RetrySuite))
    49  }
    50  
    51  func (s *RetrySuite) SetupTest() {
    52  	s.Assertions = require.New(s.T()) // Have to define our overridden assertions in the test setup. If we did it earlier, s.T() will return nil
    53  }
    54  
    55  func (s *RetrySuite) TestRetrySuccess() {
    56  	i := 0
    57  	op := func() error {
    58  		i++
    59  
    60  		if i == 5 {
    61  			return nil
    62  		}
    63  
    64  		return &someError{}
    65  	}
    66  
    67  	policy := NewExponentialRetryPolicy(1 * time.Millisecond).
    68  		WithMaximumInterval(5 * time.Millisecond).
    69  		WithMaximumAttempts(10)
    70  
    71  	err := ThrottleRetry(op, policy, nil)
    72  	s.NoError(err)
    73  	s.Equal(5, i)
    74  }
    75  
    76  func (s *RetrySuite) TestRetryFailed() {
    77  	i := 0
    78  	op := func() error {
    79  		i++
    80  
    81  		if i == 7 {
    82  			return nil
    83  		}
    84  
    85  		return &someError{}
    86  	}
    87  
    88  	policy := NewExponentialRetryPolicy(1 * time.Millisecond).
    89  		WithMaximumInterval(5 * time.Millisecond).
    90  		WithMaximumAttempts(5)
    91  
    92  	err := ThrottleRetry(op, policy, nil)
    93  	s.Error(err)
    94  }
    95  
    96  func (s *RetrySuite) TestIsRetryableSuccess() {
    97  	i := 0
    98  	op := func() error {
    99  		i++
   100  
   101  		if i == 5 {
   102  			return nil
   103  		}
   104  
   105  		return &someError{}
   106  	}
   107  
   108  	isRetryable := func(err error) bool {
   109  		if _, ok := err.(*someError); ok {
   110  			return true
   111  		}
   112  
   113  		return false
   114  	}
   115  
   116  	policy := NewExponentialRetryPolicy(1 * time.Millisecond).
   117  		WithMaximumInterval(5 * time.Millisecond).
   118  		WithMaximumAttempts(10)
   119  
   120  	err := ThrottleRetry(op, policy, isRetryable)
   121  	s.NoError(err, "Retry count: %v", i)
   122  	s.Equal(5, i)
   123  }
   124  
   125  func (s *RetrySuite) TestIsRetryableFailure() {
   126  	i := 0
   127  	theErr := someError{}
   128  	op := func() error {
   129  		i++
   130  
   131  		if i == 5 {
   132  			return nil
   133  		}
   134  
   135  		return &theErr
   136  	}
   137  
   138  	policy := NewExponentialRetryPolicy(1 * time.Millisecond).
   139  		WithMaximumInterval(5 * time.Millisecond).
   140  		WithMaximumAttempts(10)
   141  
   142  	err := ThrottleRetry(op, policy, IgnoreErrors([]error{&theErr}))
   143  	s.Error(err)
   144  	s.Equal(1, i)
   145  }
   146  
   147  func (s *RetrySuite) TestConcurrentRetrier() {
   148  	policy := NewExponentialRetryPolicy(1 * time.Millisecond).
   149  		WithMaximumInterval(10 * time.Millisecond).
   150  		WithMaximumAttempts(4)
   151  
   152  	// Basic checks
   153  	retrier := NewConcurrentRetrier(policy)
   154  	retrier.Failed()
   155  	s.Equal(int64(1), retrier.failureCount)
   156  	retrier.Succeeded()
   157  	s.Equal(int64(0), retrier.failureCount)
   158  	sleepDuration := retrier.throttleInternal()
   159  	s.Equal(done, sleepDuration)
   160  
   161  	// Multiple count check.
   162  	retrier.Failed()
   163  	retrier.Failed()
   164  	s.Equal(int64(2), retrier.failureCount)
   165  	// Verify valid sleep times.
   166  	ch := make(chan time.Duration, 3)
   167  	go func() {
   168  		for i := 0; i < 3; i++ {
   169  			ch <- retrier.throttleInternal()
   170  		}
   171  	}()
   172  	for i := 0; i < 3; i++ {
   173  		val := <-ch
   174  		fmt.Printf("Duration: %d\n", val)
   175  		s.True(val > 0)
   176  	}
   177  	retrier.Succeeded()
   178  	s.Equal(int64(0), retrier.failureCount)
   179  	// Verify we don't have any sleep times.
   180  	go func() {
   181  		for i := 0; i < 3; i++ {
   182  			ch <- retrier.throttleInternal()
   183  		}
   184  	}()
   185  	for i := 0; i < 3; i++ {
   186  		val := <-ch
   187  		fmt.Printf("Duration: %d\n", val)
   188  		s.Equal(done, val)
   189  	}
   190  }
   191  
   192  func (s *RetrySuite) TestRetryContextCancel() {
   193  	ctx, cancel := context.WithCancel(context.Background())
   194  	cancel()
   195  	err := ThrottleRetryContext(ctx, func(ctx context.Context) error { return ctx.Err() },
   196  		NewExponentialRetryPolicy(1*time.Millisecond), retryEverything)
   197  	s.ErrorIs(err, context.Canceled)
   198  }
   199  
   200  func (s *RetrySuite) TestRetryContextTimeout() {
   201  	timeout := 10 * time.Millisecond
   202  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
   203  	defer cancel()
   204  	start := time.Now()
   205  	err := ThrottleRetryContext(ctx, func(ctx context.Context) error { return &someError{} },
   206  		NewExponentialRetryPolicy(1*time.Second), retryEverything)
   207  	elapsed := time.Since(start)
   208  	s.ErrorIs(err, &someError{})
   209  	s.Less(elapsed, timeout,
   210  		"Call to retry should return early if backoff exceeds context timeout")
   211  }
   212  
   213  func (s *RetrySuite) TestContextErrorFromSomeOtherContext() {
   214  	// These errors are returned by the function being retried by they are not
   215  	// actually from the context.Context passed to RetryContext
   216  	script := []error{context.Canceled, context.DeadlineExceeded, nil}
   217  	invocation := -1
   218  	err := ThrottleRetryContext(context.Background(),
   219  		func(ctx context.Context) error {
   220  			invocation++
   221  			return script[invocation]
   222  		},
   223  		NewExponentialRetryPolicy(1*time.Millisecond),
   224  		retryEverything)
   225  	s.NoError(err)
   226  }
   227  
   228  func (s *RetrySuite) TestThrottleRetryContext() {
   229  	throttleInitialInterval := 100 * time.Millisecond
   230  	testThrottleRetryPolicy := NewExponentialRetryPolicy(throttleInitialInterval).
   231  		WithMaximumInterval(throttleRetryMaxInterval).
   232  		WithExpirationInterval(throttleRetryExpirationInterval)
   233  	originalThrottleRetryPolicy := throttleRetryPolicy
   234  	throttleRetryPolicy = testThrottleRetryPolicy
   235  
   236  	policy := NewExponentialRetryPolicy(10 * time.Millisecond).
   237  		WithMaximumAttempts(2)
   238  
   239  	// test if throttle retry policy is used on resource exhausted error
   240  	attempt := 1
   241  	op := func(_ context.Context) error {
   242  		if attempt == 1 {
   243  			attempt++
   244  			return &serviceerror.ResourceExhausted{}
   245  		}
   246  
   247  		return &someError{}
   248  	}
   249  
   250  	start := SystemClock.Now()
   251  	err := ThrottleRetryContext(context.Background(), op, policy, retryEverything)
   252  	s.Equal(&someError{}, err)
   253  	s.GreaterOrEqual(
   254  		time.Since(start),
   255  		throttleInitialInterval/2, // due to jitter
   256  		"Resource exhausted error should use throttle retry policy",
   257  	)
   258  
   259  	// test if context timeout is respected
   260  	start = SystemClock.Now()
   261  	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
   262  	err = ThrottleRetryContext(ctx, func(_ context.Context) error { return &serviceerror.ResourceExhausted{} }, policy, retryEverything)
   263  	s.Equal(&serviceerror.ResourceExhausted{}, err)
   264  	s.LessOrEqual(
   265  		time.Since(start),
   266  		throttleInitialInterval,
   267  		"Context timeout should be respected",
   268  	)
   269  	cancel()
   270  
   271  	// test if retry will stop if there's no more retry indicated by the retry policy
   272  	attempt = 0
   273  	op = func(_ context.Context) error {
   274  		attempt++
   275  		return &serviceerror.ResourceExhausted{}
   276  	}
   277  	err = ThrottleRetryContext(context.Background(), op, policy, retryEverything)
   278  	s.Equal(&serviceerror.ResourceExhausted{}, err)
   279  	s.Equal(2, attempt)
   280  
   281  	// set the default global throttle retry policy back to its original value
   282  	throttleRetryPolicy = originalThrottleRetryPolicy
   283  }
   284  
   285  var retryEverything IsRetryable = nil
   286  
   287  func (e *someError) Error() string {
   288  	return "Some Error"
   289  }