From 267302e6a9eb061306d4ba1b856a1b771d603863 Mon Sep 17 00:00:00 2001 From: shengyanli1982 Date: Mon, 25 Dec 2023 17:13:02 +0800 Subject: [PATCH] Optimize note information and some code --- .gitignore | 4 +++ options.go | 72 +++++++++++++++++++-------------------- retry.go | 98 ++++++++++++++++++++++++++++-------------------------- 3 files changed, 91 insertions(+), 83 deletions(-) diff --git a/.gitignore b/.gitignore index c40eb23..5527042 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ Gopkg.lock # cover coverage.txt + +.vscode/ +.idea/ +.DS_Store diff --git a/options.go b/options.go index 9d98a1d..318e5a7 100644 --- a/options.go +++ b/options.go @@ -7,10 +7,10 @@ import ( "time" ) -// Function signature of retry if function +// RetryIfFunc defines the function signature for the retry condition. type RetryIfFunc func(error) bool -// Function signature of OnRetry function +// OnRetryFunc defines the function signature for the on retry callback. // n = count of attempts type OnRetryFunc func(n uint, err error) @@ -44,58 +44,58 @@ type Option func(*Config) func emptyOption(c *Config) {} -// return the direct last error that came from the retried function -// default is false (return wrapped errors with everything) +// LastErrorOnly sets whether to return only the direct last error that came from the retried function. +// The default value is false (return wrapped errors with everything). func LastErrorOnly(lastErrorOnly bool) Option { return func(c *Config) { c.lastErrorOnly = lastErrorOnly } } -// Attempts set count of retry. Setting to 0 will retry until the retried function succeeds. -// default is 10 +// Attempts sets the count of retry attempts. Setting it to 0 will retry until the retried function succeeds. +// The default value is 10. func Attempts(attempts uint) Option { return func(c *Config) { c.attempts = attempts } } -// AttemptsForError sets count of retry in case execution results in given `err` -// Retries for the given `err` are also counted against total retries. -// The retry will stop if any of given retries is exhausted. +// AttemptsForError sets the count of retry attempts in case the execution results in the given `err`. +// Retries for the given `err` are also counted against the total retries. +// The retry will stop if any of the given retries is exhausted. // -// added in 4.3.0 +// Added in 4.3.0. func AttemptsForError(attempts uint, err error) Option { return func(c *Config) { c.attemptsForError[err] = attempts } } -// Delay set delay between retry -// default is 100ms +// Delay sets the delay between retries. +// The default value is 100ms. func Delay(delay time.Duration) Option { return func(c *Config) { c.delay = delay } } -// MaxDelay set maximum delay between retry -// does not apply by default +// MaxDelay sets the maximum delay between retries. +// It does not apply by default. func MaxDelay(maxDelay time.Duration) Option { return func(c *Config) { c.maxDelay = maxDelay } } -// MaxJitter sets the maximum random Jitter between retries for RandomDelay +// MaxJitter sets the maximum random jitter between retries for RandomDelay. func MaxJitter(maxJitter time.Duration) Option { return func(c *Config) { c.maxJitter = maxJitter } } -// DelayType set type of the delay between retries -// default is BackOff +// DelayType sets the type of delay between retries. +// The default value is BackOff. func DelayType(delayType DelayTypeFunc) Option { if delayType == nil { return emptyOption @@ -105,7 +105,7 @@ func DelayType(delayType DelayTypeFunc) Option { } } -// BackOffDelay is a DelayType which increases delay between consecutive retries +// BackOffDelay is a DelayType which increases the delay between consecutive retries. func BackOffDelay(n uint, _ error, config *Config) time.Duration { // 1 << 63 would overflow signed int64 (time.Duration), thus 62. const max uint = 62 @@ -125,17 +125,17 @@ func BackOffDelay(n uint, _ error, config *Config) time.Duration { return config.delay << n } -// FixedDelay is a DelayType which keeps delay the same through all iterations +// FixedDelay is a DelayType which keeps the delay the same through all iterations. func FixedDelay(_ uint, _ error, config *Config) time.Duration { return config.delay } -// RandomDelay is a DelayType which picks a random delay up to config.maxJitter +// RandomDelay is a DelayType which picks a random delay up to config.maxJitter. func RandomDelay(_ uint, _ error, config *Config) time.Duration { return time.Duration(rand.Int63n(int64(config.maxJitter))) } -// CombineDelay is a DelayType the combines all of the specified delays into a new DelayTypeFunc +// CombineDelay is a DelayType that combines all of the specified delays into a new DelayTypeFunc. func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc { const maxInt64 = uint64(math.MaxInt64) @@ -152,9 +152,9 @@ func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc { } } -// OnRetry function callback are called each retry +// OnRetry sets the callback function that is called on each retry. // -// log each retry example: +// Example of logging each retry: // // retry.Do( // func() error { @@ -174,9 +174,9 @@ func OnRetry(onRetry OnRetryFunc) Option { } // RetryIf controls whether a retry should be attempted after an error -// (assuming there are any retry attempts remaining) +// (assuming there are any retry attempts remaining). // -// skip retry if special error example: +// Example of skipping retry for a special error: // // retry.Do( // func() error { @@ -190,8 +190,8 @@ func OnRetry(onRetry OnRetryFunc) Option { // }) // ) // -// By default RetryIf stops execution if the error is wrapped using `retry.Unrecoverable`, -// so above example may also be shortened to: +// By default, RetryIf stops execution if the error is wrapped using `retry.Unrecoverable`. +// So the above example may also be shortened to: // // retry.Do( // func() error { @@ -207,10 +207,10 @@ func RetryIf(retryIf RetryIfFunc) Option { } } -// Context allow to set context of retry -// default are Background context +// Context allows setting the context of the retry. +// The default context is the Background context. // -// example of immediately cancellation (maybe it isn't the best example, but it describes behavior enough; I hope) +// Example of immediate cancellation (maybe it isn't the best example, but it describes the behavior enough; I hope): // // ctx, cancel := context.WithCancel(context.Background()) // cancel() @@ -228,12 +228,12 @@ func Context(ctx context.Context) Option { } // WithTimer provides a way to swap out timer module implementations. -// This primarily is useful for mocking/testing, where you may not want to explicitly wait for a set duration +// This is primarily useful for mocking/testing, where you may not want to explicitly wait for a set duration // for retries. // -// example of augmenting time.After with a print statement +// Example of augmenting time.After with a print statement: // -// type struct MyTimer {} +// type MyTimer struct {} // // func (t *MyTimer) After(d time.Duration) <- chan time.Time { // fmt.Print("Timer called!") @@ -251,10 +251,10 @@ func WithTimer(t Timer) Option { } // WrapContextErrorWithLastError allows the context error to be returned wrapped with the last error that the -// retried function returned. This is only applicable when Attempts is set to 0 to retry indefinitly and when -// using a context to cancel / timeout +// retried function returned. This is only applicable when Attempts is set to 0 to retry indefinitely and when +// using a context to cancel / timeout. // -// default is false +// The default value is false. // // ctx, cancel := context.WithCancel(context.Background()) // defer cancel() diff --git a/retry.go b/retry.go index 56bc9dd..aef3bea 100644 --- a/retry.go +++ b/retry.go @@ -87,6 +87,7 @@ http get with retry with data: - `retry.Retry` function are changed to `retry.Do` function - `retry.RetryCustom` (OnRetry) and `retry.RetryCustomWithOpts` functions are now implement via functions produces Options (aka `retry.OnRetry`) */ + package retry import ( @@ -110,6 +111,7 @@ func (t *timerImpl) After(d time.Duration) <-chan time.Time { return time.After(d) } +// Do executes the retryable function with the given options func Do(retryableFunc RetryableFunc, opts ...Option) error { retryableFuncWithData := func() (any, error) { return nil, retryableFunc() @@ -119,50 +121,45 @@ func Do(retryableFunc RetryableFunc, opts ...Option) error { return err } -func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error) { - var n uint - var emptyT T +// DoWithData executes the retryable function with data with the given options +func DoWithData[T any](retryableFunc RetryableFuncWithData[T], options ...Option) (T, error) { + var attemptCount uint + var emptyResult T - // default + // Default configuration config := newDefaultRetryConfig() - // apply opts - for _, opt := range opts { + // Apply options + for _, opt := range options { opt(config) } + // Check if context is already done if err := config.context.Err(); err != nil { - return emptyT, err + return emptyResult, err } // Setting attempts to 0 means we'll retry until we succeed - var lastErr error if config.attempts == 0 { for { - t, err := retryableFunc() + result, err := retryableFunc() if err == nil { - return t, nil - } - - if !IsRecoverable(err) { - return emptyT, err + return result, nil } - if !config.retryIf(err) { - return emptyT, err + if !config.retryIf(err) || !IsRecoverable(err) { + return emptyResult, err } - lastErr = err - - n++ - config.onRetry(n, err) + attemptCount++ + config.onRetry(attemptCount, err) select { - case <-config.timer.After(delay(config, n, err)): + case <-config.timer.After(delay(config, attemptCount, err)): case <-config.context.Done(): if config.wrapContextErrorWithLastError { - return emptyT, Error{config.context.Err(), lastErr} + return emptyResult, Error{config.context.Err(), err} } - return emptyT, config.context.Err() + return emptyResult, config.context.Err() } } } @@ -175,10 +172,15 @@ func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) ( } shouldRetry := true + errorCounts := make(map[error]uint, len(attemptsForError)) + for err, attempts := range attemptsForError { + errorCounts[err] = attempts + } + for shouldRetry { - t, err := retryableFunc() + result, err := retryableFunc() if err == nil { - return t, nil + return result, nil } errorLog = append(errorLog, unpackUnrecoverable(err)) @@ -187,41 +189,41 @@ func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) ( break } - config.onRetry(n, err) + config.onRetry(attemptCount, err) // onRetry should be called before checking attempts - for errToCheck, attempts := range attemptsForError { - if errors.Is(err, errToCheck) { - attempts-- - attemptsForError[errToCheck] = attempts - shouldRetry = shouldRetry && attempts > 0 - } + if attempts, ok := errorCounts[err]; ok { + attempts-- + errorCounts[err] = attempts + shouldRetry = shouldRetry && attempts > 0 } - // if this is last attempt - don't wait - if n == config.attempts-1 { + // If this is the last attempt, don't wait + if attemptCount == config.attempts-1 { break } select { - case <-config.timer.After(delay(config, n, err)): + case <-config.timer.After(delay(config, attemptCount, err)): case <-config.context.Done(): if config.lastErrorOnly { - return emptyT, config.context.Err() + return emptyResult, config.context.Err() } - return emptyT, append(errorLog, config.context.Err()) + return emptyResult, append(errorLog, config.context.Err()) } - n++ - shouldRetry = shouldRetry && n < config.attempts + attemptCount++ + shouldRetry = shouldRetry && attemptCount < config.attempts } if config.lastErrorOnly { - return emptyT, errorLog.Unwrap() + return emptyResult, errorLog.Unwrap() } - return emptyT, errorLog + + return emptyResult, errorLog } +// newDefaultRetryConfig returns the default configuration for retry func newDefaultRetryConfig() *Config { return &Config{ attempts: uint(10), @@ -243,16 +245,17 @@ type Error []error // Error method return string representation of Error // It is an implementation of error interface func (e Error) Error() string { - logWithNumber := make([]string, len(e)) - for i, l := range e { - if l != nil { - logWithNumber[i] = fmt.Sprintf("#%d: %s", i+1, l.Error()) + logsWithNumbers := make([]string, len(e)) + for i, err := range e { + if err != nil { + logsWithNumbers[i] = fmt.Sprintf("#%d: %s", i+1, err.Error()) } } - return fmt.Sprintf("All attempts fail:\n%s", strings.Join(logWithNumber, "\n")) + return fmt.Sprintf("All attempts fail:\n%s", strings.Join(logsWithNumbers, "\n")) } +// Is checks if the error list contains the target error func (e Error) Is(target error) bool { for _, v := range e { if errors.Is(v, target) { @@ -262,6 +265,7 @@ func (e Error) Is(target error) bool { return false } +// As checks if the error list contains an error assignable to the target type func (e Error) As(target interface{}) bool { for _, v := range e { if errors.As(v, target) { @@ -323,7 +327,7 @@ func IsRecoverable(err error) bool { return !errors.Is(err, unrecoverableError{}) } -// Adds support for errors.Is usage on unrecoverableError +// Is checks if the error is an instance of `unrecoverableError` func (unrecoverableError) Is(err error) bool { _, isUnrecoverable := err.(unrecoverableError) return isUnrecoverable