git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/retry/retry.go (about)

     1  /*
     2  Simple library for retry mechanism
     3  
     4  slightly inspired by [Try::Tiny::Retry](https://metacpan.org/pod/Try::Tiny::Retry)
     5  
     6  # SYNOPSIS
     7  
     8  http get with retry:
     9  
    10  	url := "http://example.com"
    11  	var body []byte
    12  
    13  	err := retry.Do(
    14  		func() error {
    15  			resp, err := http.Get(url)
    16  			if err != nil {
    17  				return err
    18  			}
    19  			defer resp.Body.Close()
    20  			body, err = ioutil.ReadAll(resp.Body)
    21  			if err != nil {
    22  				return err
    23  			}
    24  
    25  			return nil
    26  		},
    27  	)
    28  
    29  	fmt.Println(body)
    30  
    31  [next examples](https://git.sr.ht/~pingoo/stdx/retrytree/master/examples)
    32  
    33  # SEE ALSO
    34  
    35  * [giantswarm/retry-go](https://github.com/giantswarm/retry-go) - slightly complicated interface.
    36  
    37  * [sethgrid/pester](https://github.com/sethgrid/pester) - only http retry for http calls with retries and backoff
    38  
    39  * [cenkalti/backoff](https://github.com/cenkalti/backoff) - Go port of the exponential backoff algorithm from Google's HTTP Client Library for Java. Really complicated interface.
    40  
    41  * [rafaeljesus/retry-go](https://github.com/rafaeljesus/retry-go) - looks good, slightly similar as this package, don't have 'simple' `Retry` method
    42  
    43  * [matryer/try](https://github.com/matryer/try) - very popular package, nonintuitive interface (for me)
    44  
    45  # BREAKING CHANGES
    46  
    47  * 4.0.0
    48    - infinity retry is possible by set `Attempts(0)` by PR [#49](https://git.sr.ht/~pingoo/stdx/retrypull/49)
    49  
    50  * 3.0.0
    51    - `DelayTypeFunc` accepts a new parameter `err` - this breaking change affects only your custom Delay Functions. This change allow [make delay functions based on error](examples/delay_based_on_error_test.go).
    52  
    53  * 1.0.2 -> 2.0.0
    54    - argument of `retry.Delay` is final delay (no multiplication by `retry.Units` anymore)
    55    - function `retry.Units` are removed
    56    - [more about this breaking change](https://git.sr.ht/~pingoo/stdx/retryissues/7)
    57  
    58  * 0.3.0 -> 1.0.0
    59    - `retry.Retry` function are changed to `retry.Do` function
    60    - `retry.RetryCustom` (OnRetry) and `retry.RetryCustomWithOpts` functions are now implement via functions produces Options (aka `retry.OnRetry`)
    61  */
    62  package retry
    63  
    64  import (
    65  	"context"
    66  	"fmt"
    67  	"strings"
    68  	"time"
    69  )
    70  
    71  // Function signature of retryable function
    72  type RetryableFunc func() error
    73  
    74  func Do(retryableFunc RetryableFunc, opts ...Option) error {
    75  	var n uint
    76  
    77  	// default
    78  	config := newDefaultRetryConfig()
    79  
    80  	// apply opts
    81  	for _, opt := range opts {
    82  		opt(config)
    83  	}
    84  
    85  	if err := config.context.Err(); err != nil {
    86  		return err
    87  	}
    88  
    89  	// Setting attempts to 0 means we'll retry until we succeed
    90  	if config.attempts == 0 {
    91  		for err := retryableFunc(); err != nil; err = retryableFunc() {
    92  			n++
    93  
    94  			config.onRetry(n, err)
    95  
    96  			<-time.After(delay(config, n, err))
    97  		}
    98  
    99  		return nil
   100  	}
   101  
   102  	var errorLog Error
   103  	if !config.lastErrorOnly {
   104  		errorLog = make(Error, config.attempts)
   105  	} else {
   106  		errorLog = make(Error, 1)
   107  	}
   108  
   109  	lastErrIndex := n
   110  	for n < config.attempts {
   111  		err := retryableFunc()
   112  
   113  		if err != nil {
   114  			errorLog[lastErrIndex] = unpackUnrecoverable(err)
   115  
   116  			if !config.retryIf(err) {
   117  				break
   118  			}
   119  
   120  			config.onRetry(n, err)
   121  
   122  			// if this is last attempt - don't wait
   123  			if n == config.attempts-1 {
   124  				break
   125  			}
   126  
   127  			select {
   128  			case <-time.After(delay(config, n, err)):
   129  			case <-config.context.Done():
   130  				if config.lastErrorOnly {
   131  					return config.context.Err()
   132  				}
   133  				errorLog[n] = config.context.Err()
   134  				return errorLog
   135  			}
   136  
   137  		} else {
   138  			return nil
   139  		}
   140  
   141  		n++
   142  		if !config.lastErrorOnly {
   143  			lastErrIndex = n
   144  		}
   145  	}
   146  
   147  	if config.lastErrorOnly {
   148  		return errorLog[lastErrIndex]
   149  	}
   150  	return errorLog
   151  }
   152  
   153  func newDefaultRetryConfig() *Config {
   154  	return &Config{
   155  		attempts:      uint(10),
   156  		delay:         100 * time.Millisecond,
   157  		maxJitter:     100 * time.Millisecond,
   158  		onRetry:       func(n uint, err error) {},
   159  		retryIf:       IsRecoverable,
   160  		delayType:     CombineDelay(BackOffDelay, RandomDelay),
   161  		lastErrorOnly: false,
   162  		context:       context.Background(),
   163  	}
   164  }
   165  
   166  // Error type represents list of errors in retry
   167  type Error []error
   168  
   169  // Error method return string representation of Error
   170  // It is an implementation of error interface
   171  func (e Error) Error() string {
   172  	logWithNumber := make([]string, lenWithoutNil(e))
   173  	for i, l := range e {
   174  		if l != nil {
   175  			logWithNumber[i] = fmt.Sprintf("#%d: %s", i+1, l.Error())
   176  		}
   177  	}
   178  
   179  	return fmt.Sprintf("All attempts fail:\n%s", strings.Join(logWithNumber, "\n"))
   180  }
   181  
   182  func lenWithoutNil(e Error) (count int) {
   183  	for _, v := range e {
   184  		if v != nil {
   185  			count++
   186  		}
   187  	}
   188  
   189  	return
   190  }
   191  
   192  // WrappedErrors returns the list of errors that this Error is wrapping.
   193  // It is an implementation of the `errwrap.Wrapper` interface
   194  // in package [errwrap](https://github.com/hashicorp/errwrap) so that
   195  // `retry.Error` can be used with that library.
   196  func (e Error) WrappedErrors() []error {
   197  	return e
   198  }
   199  
   200  type unrecoverableError struct {
   201  	error
   202  }
   203  
   204  // Unrecoverable wraps an error in `unrecoverableError` struct
   205  func Unrecoverable(err error) error {
   206  	return unrecoverableError{err}
   207  }
   208  
   209  // IsRecoverable checks if error is an instance of `unrecoverableError`
   210  func IsRecoverable(err error) bool {
   211  	_, isUnrecoverable := err.(unrecoverableError)
   212  	return !isUnrecoverable
   213  }
   214  
   215  func unpackUnrecoverable(err error) error {
   216  	if unrecoverable, isUnrecoverable := err.(unrecoverableError); isUnrecoverable {
   217  		return unrecoverable.error
   218  	}
   219  
   220  	return err
   221  }
   222  
   223  func delay(config *Config, n uint, err error) time.Duration {
   224  	delayTime := config.delayType(n, err, config)
   225  	if config.maxDelay > 0 && delayTime > config.maxDelay {
   226  		delayTime = config.maxDelay
   227  	}
   228  
   229  	return delayTime
   230  }