github.com/go-playground/pkg/v5@v5.29.1/errors/retrier_test.go (about)

     1  //go:build go1.18
     2  // +build go1.18
     3  
     4  package errorsext
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"io"
    10  	"testing"
    11  	"time"
    12  
    13  	. "github.com/go-playground/assert/v2"
    14  	. "github.com/go-playground/pkg/v5/values/result"
    15  )
    16  
    17  // TODO: Add IsRetryable and Retryable to helper functions.
    18  
    19  func TestRetrierMaxAttempts(t *testing.T) {
    20  	var i, j int
    21  	result := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) {
    22  		j++
    23  	}).MaxAttempts(MaxAttempts, 3).Do(context.Background(), func(ctx context.Context) Result[int, error] {
    24  		i++
    25  		if i > 50 {
    26  			panic("infinite loop")
    27  		}
    28  		return Err[int, error](io.EOF)
    29  	})
    30  	Equal(t, result.IsErr(), true)
    31  	Equal(t, result.Err(), io.EOF)
    32  	Equal(t, i, 3)
    33  	Equal(t, j, 2)
    34  }
    35  
    36  func TestRetrierMaxAttemptsNonRetryable(t *testing.T) {
    37  	var i, j int
    38  	returnErr := io.ErrUnexpectedEOF
    39  	result := NewRetryer[int, error]().IsRetryableFn(func(_ context.Context, e error) (isRetryable bool) {
    40  		if returnErr == io.EOF {
    41  			return false
    42  		} else {
    43  			return true
    44  		}
    45  	}).Backoff(func(ctx context.Context, attempt int, _ error) {
    46  		j++
    47  		if j == 10 {
    48  			returnErr = io.EOF
    49  		}
    50  	}).MaxAttempts(MaxAttemptsNonRetryable, 3).Do(context.Background(), func(ctx context.Context) Result[int, error] {
    51  		i++
    52  		if i > 50 {
    53  			panic("infinite loop")
    54  		}
    55  		return Err[int, error](returnErr)
    56  	})
    57  	Equal(t, result.IsErr(), true)
    58  	Equal(t, result.Err(), io.EOF)
    59  	Equal(t, i, 13)
    60  	Equal(t, j, 12)
    61  }
    62  
    63  func TestRetrierMaxAttemptsNonRetryableReset(t *testing.T) {
    64  	var i, j int
    65  	returnErr := io.EOF
    66  	result := NewRetryer[int, error]().IsRetryableFn(func(_ context.Context, e error) (isRetryable bool) {
    67  		if returnErr == io.EOF {
    68  			return false
    69  		} else {
    70  			return true
    71  		}
    72  	}).Backoff(func(ctx context.Context, attempt int, _ error) {
    73  		j++
    74  		if j == 2 {
    75  			returnErr = io.ErrUnexpectedEOF
    76  		} else if j == 10 {
    77  			returnErr = io.EOF
    78  		}
    79  	}).MaxAttempts(MaxAttemptsNonRetryableReset, 3).Do(context.Background(), func(ctx context.Context) Result[int, error] {
    80  		i++
    81  		if i > 50 {
    82  			panic("infinite loop")
    83  		}
    84  		return Err[int, error](returnErr)
    85  	})
    86  	Equal(t, result.IsErr(), true)
    87  	Equal(t, result.Err(), io.EOF)
    88  	Equal(t, i, 13)
    89  	Equal(t, j, 12)
    90  }
    91  
    92  func TestRetrierMaxAttemptsUnlimited(t *testing.T) {
    93  	var i, j int
    94  	r := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) {
    95  		j++
    96  	}).MaxAttempts(MaxAttemptsUnlimited, 0)
    97  
    98  	PanicMatches(t, func() {
    99  		r.Do(context.Background(), func(ctx context.Context) Result[int, error] {
   100  			i++
   101  			if i > 50 {
   102  				panic("infinite loop")
   103  			}
   104  			return Err[int, error](io.EOF)
   105  		})
   106  	}, "infinite loop")
   107  }
   108  
   109  func TestRetrierMaxAttemptsTimeout(t *testing.T) {
   110  	result := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) {
   111  	}).MaxAttempts(MaxAttempts, 1).Timeout(time.Second).
   112  		Do(context.Background(), func(ctx context.Context) Result[int, error] {
   113  			select {
   114  			case <-ctx.Done():
   115  				return Err[int, error](ctx.Err())
   116  			case <-time.After(time.Second * 3):
   117  				return Err[int, error](io.EOF)
   118  			}
   119  		})
   120  	Equal(t, result.IsErr(), true)
   121  	Equal(t, result.Err(), context.DeadlineExceeded)
   122  }
   123  
   124  func TestRetrierEarlyReturn(t *testing.T) {
   125  	var earlyReturnCount int
   126  
   127  	r := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) {
   128  	}).MaxAttempts(MaxAttempts, 5).Timeout(time.Second).
   129  		IsEarlyReturnFn(func(ctx context.Context, err error) bool {
   130  			earlyReturnCount++
   131  			return errors.Is(err, io.EOF)
   132  		}).Backoff(nil)
   133  
   134  	result := r.Do(context.Background(), func(ctx context.Context) Result[int, error] {
   135  		return Err[int, error](io.EOF)
   136  	})
   137  	Equal(t, result.IsErr(), true)
   138  	Equal(t, result.Err(), io.EOF)
   139  	Equal(t, earlyReturnCount, 1)
   140  
   141  	// now let try with retryable overriding early return TL;DR retryable should take precedence over early return
   142  	earlyReturnCount = 0
   143  	isRetryableCount := 0
   144  	result = r.IsRetryableFn(func(ctx context.Context, err error) (isRetryable bool) {
   145  		isRetryableCount++
   146  		return errors.Is(err, io.EOF)
   147  	}).Do(context.Background(), func(ctx context.Context) Result[int, error] {
   148  		return Err[int, error](io.EOF)
   149  	})
   150  	Equal(t, result.IsErr(), true)
   151  	Equal(t, result.Err(), io.EOF)
   152  	Equal(t, earlyReturnCount, 0)
   153  	Equal(t, isRetryableCount, 5)
   154  
   155  	// while here let's check the first test case again, `Retrier` should be a copy and original still intact.
   156  	isRetryableCount = 0
   157  	result = r.Do(context.Background(), func(ctx context.Context) Result[int, error] {
   158  		return Err[int, error](io.EOF)
   159  	})
   160  	Equal(t, result.IsErr(), true)
   161  	Equal(t, result.Err(), io.EOF)
   162  	Equal(t, earlyReturnCount, 1)
   163  	Equal(t, isRetryableCount, 0)
   164  }