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

     1  package retry
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  	"math/rand"
     7  	"time"
     8  )
     9  
    10  // Function signature of retry if function
    11  type RetryIfFunc func(error) bool
    12  
    13  // Function signature of OnRetry function
    14  // n = count of attempts
    15  type OnRetryFunc func(n uint, err error)
    16  
    17  // DelayTypeFunc is called to return the next delay to wait after the retriable function fails on `err` after `n` attempts.
    18  type DelayTypeFunc func(n uint, err error, config *Config) time.Duration
    19  
    20  type Config struct {
    21  	attempts      uint
    22  	delay         time.Duration
    23  	maxDelay      time.Duration
    24  	maxJitter     time.Duration
    25  	onRetry       OnRetryFunc
    26  	retryIf       RetryIfFunc
    27  	delayType     DelayTypeFunc
    28  	lastErrorOnly bool
    29  	context       context.Context
    30  
    31  	maxBackOffN uint
    32  }
    33  
    34  // Option represents an option for retry.
    35  type Option func(*Config)
    36  
    37  func emptyOption(c *Config) {}
    38  
    39  // return the direct last error that came from the retried function
    40  // default is false (return wrapped errors with everything)
    41  func LastErrorOnly(lastErrorOnly bool) Option {
    42  	return func(c *Config) {
    43  		c.lastErrorOnly = lastErrorOnly
    44  	}
    45  }
    46  
    47  // Attempts set count of retry. Setting to 0 will retry until the retried function succeeds.
    48  // default is 10
    49  func Attempts(attempts uint) Option {
    50  	return func(c *Config) {
    51  		c.attempts = attempts
    52  	}
    53  }
    54  
    55  // Delay set delay between retry
    56  // default is 100ms
    57  func Delay(delay time.Duration) Option {
    58  	return func(c *Config) {
    59  		c.delay = delay
    60  	}
    61  }
    62  
    63  // MaxDelay set maximum delay between retry
    64  // does not apply by default
    65  func MaxDelay(maxDelay time.Duration) Option {
    66  	return func(c *Config) {
    67  		c.maxDelay = maxDelay
    68  	}
    69  }
    70  
    71  // MaxJitter sets the maximum random Jitter between retries for RandomDelay
    72  func MaxJitter(maxJitter time.Duration) Option {
    73  	return func(c *Config) {
    74  		c.maxJitter = maxJitter
    75  	}
    76  }
    77  
    78  // DelayType set type of the delay between retries
    79  // default is BackOff
    80  func DelayType(delayType DelayTypeFunc) Option {
    81  	if delayType == nil {
    82  		return emptyOption
    83  	}
    84  	return func(c *Config) {
    85  		c.delayType = delayType
    86  	}
    87  }
    88  
    89  // BackOffDelay is a DelayType which increases delay between consecutive retries
    90  func BackOffDelay(n uint, _ error, config *Config) time.Duration {
    91  	// 1 << 63 would overflow signed int64 (time.Duration), thus 62.
    92  	const max uint = 62
    93  
    94  	if config.maxBackOffN == 0 {
    95  		if config.delay <= 0 {
    96  			config.delay = 1
    97  		}
    98  
    99  		config.maxBackOffN = max - uint(math.Floor(math.Log2(float64(config.delay))))
   100  	}
   101  
   102  	if n > config.maxBackOffN {
   103  		n = config.maxBackOffN
   104  	}
   105  
   106  	return config.delay << n
   107  }
   108  
   109  // FixedDelay is a DelayType which keeps delay the same through all iterations
   110  func FixedDelay(_ uint, _ error, config *Config) time.Duration {
   111  	return config.delay
   112  }
   113  
   114  // RandomDelay is a DelayType which picks a random delay up to config.maxJitter
   115  func RandomDelay(_ uint, _ error, config *Config) time.Duration {
   116  	return time.Duration(rand.Int63n(int64(config.maxJitter)))
   117  }
   118  
   119  // CombineDelay is a DelayType the combines all of the specified delays into a new DelayTypeFunc
   120  func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc {
   121  	const maxInt64 = uint64(math.MaxInt64)
   122  
   123  	return func(n uint, err error, config *Config) time.Duration {
   124  		var total uint64
   125  		for _, delay := range delays {
   126  			total += uint64(delay(n, err, config))
   127  			if total > maxInt64 {
   128  				total = maxInt64
   129  			}
   130  		}
   131  
   132  		return time.Duration(total)
   133  	}
   134  }
   135  
   136  // OnRetry function callback are called each retry
   137  //
   138  // log each retry example:
   139  //
   140  //	retry.Do(
   141  //		func() error {
   142  //			return errors.New("some error")
   143  //		},
   144  //		retry.OnRetry(func(n uint, err error) {
   145  //			log.Printf("#%d: %s\n", n, err)
   146  //		}),
   147  //	)
   148  func OnRetry(onRetry OnRetryFunc) Option {
   149  	if onRetry == nil {
   150  		return emptyOption
   151  	}
   152  	return func(c *Config) {
   153  		c.onRetry = onRetry
   154  	}
   155  }
   156  
   157  // RetryIf controls whether a retry should be attempted after an error
   158  // (assuming there are any retry attempts remaining)
   159  //
   160  // skip retry if special error example:
   161  //
   162  //	retry.Do(
   163  //		func() error {
   164  //			return errors.New("special error")
   165  //		},
   166  //		retry.RetryIf(func(err error) bool {
   167  //			if err.Error() == "special error" {
   168  //				return false
   169  //			}
   170  //			return true
   171  //		})
   172  //	)
   173  //
   174  // By default RetryIf stops execution if the error is wrapped using `retry.Unrecoverable`,
   175  // so above example may also be shortened to:
   176  //
   177  //	retry.Do(
   178  //		func() error {
   179  //			return retry.Unrecoverable(errors.New("special error"))
   180  //		}
   181  //	)
   182  func RetryIf(retryIf RetryIfFunc) Option {
   183  	if retryIf == nil {
   184  		return emptyOption
   185  	}
   186  	return func(c *Config) {
   187  		c.retryIf = retryIf
   188  	}
   189  }
   190  
   191  // Context allow to set context of retry
   192  // default are Background context
   193  //
   194  // example of immediately cancellation (maybe it isn't the best example, but it describes behavior enough; I hope)
   195  //
   196  //	ctx, cancel := context.WithCancel(context.Background())
   197  //	cancel()
   198  //
   199  //	retry.Do(
   200  //		func() error {
   201  //			...
   202  //		},
   203  //		retry.Context(ctx),
   204  //	)
   205  func Context(ctx context.Context) Option {
   206  	return func(c *Config) {
   207  		c.context = ctx
   208  	}
   209  }