diff --git a/.golangci.yaml b/.golangci.yaml index c1a7f12..bad6894 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -274,7 +274,8 @@ issues: # Default dirs are skipped independently of this option's value (see exclude-dirs-use-default). # "/" will be replaced by current OS file path separator to properly work on Windows. # Default: [] - exclude-dirs: [] + exclude-dirs: + - examples # Show issues in any part of update files (requires new-from-rev or new-from-patch). # Default: false whole-files: false diff --git a/README.md b/README.md index 6ed4298..8fbaf9a 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ func main() { retrier.WithMinDelay(100*time.Millisecond), retrier.WithMaxDelay(1*time.Second), retrier.WithBackoff(backoff.ExponentialWithDecorrelatedJitter()), + retrier.WithNotifier(func(err error, backoff time.Duration) { + fmt.Printf("Operation failed: %v\n", err) + fmt.Printf("...wait %d seconds for the next retry\n\n", backoff) + }), ) if err != nil { @@ -89,6 +93,7 @@ The following options can be used to customize the retry behavior: * `WithMinDelay(time.Duration)`: Sets the minimum delay between retries. * `WithMaxDelay(time.Duration)`: Sets the maximum delay between retries. * `WithBackoff(backoff.Backoff)`: Sets the backoff strategy to be used. +* `WithNotifier(notifier)`: Sets a callback function that gets triggered on each retry attempt, providing feedback on errors and backoff. ## Contributing diff --git a/examples/basic/main.go b/examples/basic/main.go new file mode 100644 index 0000000..09c83dd --- /dev/null +++ b/examples/basic/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "errors" + "fmt" + "time" + + retrier "github.com/hueristiq/hq-go-retrier" + "github.com/hueristiq/hq-go-retrier/backoff" +) + +func main() { + operation := func() error { + // Simulate a failing operation + fmt.Println("Trying operation...") + return errors.New("operation failed") + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Retry the operation with custom configuration + err := retrier.Retry(ctx, operation, + retrier.WithMaxRetries(5), + retrier.WithMinDelay(100*time.Millisecond), + retrier.WithMaxDelay(1*time.Second), + retrier.WithBackoff(backoff.ExponentialWithDecorrelatedJitter()), + retrier.WithNotifier(func(err error, backoff time.Duration) { + fmt.Printf("Operation failed: %v\n", err) + fmt.Printf("...wait %d seconds for the next retry\n\n", backoff) + }), + ) + + if err != nil { + fmt.Printf("Operation failed after retries: %v\n", err) + } else { + fmt.Println("Operation succeeded") + } +} diff --git a/retrier.go b/retrier.go index 0c192cb..a06e104 100644 --- a/retrier.go +++ b/retrier.go @@ -15,15 +15,38 @@ import ( // - minDelay: The minimum delay between retries. // - maxDelay: The maximum allowable delay between retries. // - backoff: A function that calculates the backoff duration based on retry attempt number and delay limits. +// - notifier: A callback function that gets triggered on each retry attempt, providing feedback on errors and backoff duration. type Configuration struct { - maxRetries int // Maximum number of retry attempts. - minDelay time.Duration // Minimum delay between retry attempts. - maxDelay time.Duration // Maximum delay between retry attempts. - backoff backoff.Backoff // Backoff strategy used to calculate delay between attempts. + maxRetries int + minDelay time.Duration + maxDelay time.Duration + backoff backoff.Backoff + notifier Notifer } +// Notifer is a callback function type used to handle notifications during retry attempts. +// This function is invoked on every retry attempt, providing details about the error that +// triggered the retry and the calculated backoff duration before the next attempt. +// +// Parameters: +// - err: The error encountered in the current retry attempt. +// - backoff: The duration of backoff calculated before the next retry attempt. +// +// Example: +// +// func logNotifier(err error, backoff time.Duration) { +// fmt.Printf("Retrying after error: %v, backoff: %v\n", err, backoff) +// } +type Notifer func(err error, backoff time.Duration) + // Option is a function type used to modify the Configuration of the retrier. Options allow // for the flexible configuration of retry policies by applying user-defined settings. +// +// Parameters: +// - *Configuration: A pointer to the Configuration struct that allows modification of its fields. +// +// Returns: +// - Option: A functional option that modifies the Configuration struct, allowing customization of retry behavior. type Option func(*Configuration) // WithMaxRetries sets the maximum number of retries for the retry mechanism. When the specified @@ -98,3 +121,22 @@ func WithBackoff(strategy backoff.Backoff) Option { c.backoff = strategy } } + +// WithNotifier sets a notifier callback function that gets called on each retry attempt. This function +// allows users to log, monitor, or perform any action upon each retry attempt by providing error details +// and the duration of the backoff period. +// +// Parameters: +// - notifier: A function of type Notifer that will be called on each retry with the error and backoff duration. +// +// Returns: +// - Option: A functional option that modifies the Configuration to set the notifier function. +// +// Example: +// +// retrier.WithNotifier(logNotifier) sets up a notifier that logs each retry attempt. +func WithNotifier(notifier Notifer) Option { + return func(c *Configuration) { + c.notifier = notifier + } +} diff --git a/retry.go b/retry.go index 01242fa..a9fe57b 100644 --- a/retry.go +++ b/retry.go @@ -14,6 +14,10 @@ type Operation func() (err error) // withEmptyData wraps an Operation function to convert it into an OperationWithData that // returns an empty struct. This is used for cases where the operation does not return any data // but can be retried with the same mechanism as data-returning operations. +// +// Returns: +// - operationWithData: An OperationWithData function that returns an empty struct and error, +// allowing non-data-returning operations to be handled by the RetryWithData function. func (o Operation) withEmptyData() (operationWithData OperationWithData[struct{}]) { operationWithData = func() (struct{}, error) { return struct{}{}, o() @@ -45,6 +49,7 @@ type OperationWithData[T any] func() (data T, err error) // err := retrier.Retry(ctx, someOperation, retrier.WithMaxRetries(5), retrier.WithBackoff(backoff.Exponential())) // // Retries 'someOperation' up to 5 times with exponential backoff. func Retry(ctx context.Context, operation Operation, opts ...Option) (err error) { + // Use RetryWithData with an empty struct as a workaround for non-data-returning operations. _, err = RetryWithData(ctx, operation.withEmptyData(), opts...) return @@ -68,20 +73,17 @@ func Retry(ctx context.Context, operation Operation, opts ...Option) (err error) // result, err := retrier.RetryWithData(ctx, fetchData, retrier.WithMaxRetries(5), retrier.WithBackoff(backoff.Exponential())) // // Retries 'fetchData' up to 5 times with exponential backoff. func RetryWithData[T any](ctx context.Context, operation OperationWithData[T], opts ...Option) (result T, err error) { - // Set default retry configuration. cfg := &Configuration{ - maxRetries: 3, // Default maximum retries - maxDelay: 1000 * time.Millisecond, // Default maximum delay between retries - minDelay: 100 * time.Millisecond, // Default minimum delay between retries - backoff: backoff.Exponential(), // Default backoff strategy: exponential + maxRetries: 3, + maxDelay: 1000 * time.Millisecond, + minDelay: 100 * time.Millisecond, + backoff: backoff.Exponential(), } - // Apply any provided options to configure retry behavior. for _, opt := range opts { opt(cfg) } - // Retry loop up to maxRetries. for attempt := range cfg.maxRetries { select { case <-ctx.Done(): @@ -100,15 +102,20 @@ func RetryWithData[T any](ctx context.Context, operation OperationWithData[T], o // If the operation fails, calculate the backoff delay. b := cfg.backoff(cfg.minDelay, cfg.maxDelay, attempt) + // Trigger notifier if configured, providing feedback on the error and backoff duration. + if cfg.notifier != nil { + cfg.notifier(err, b) + } + // Wait for the backoff period before the next retry attempt. ticker := time.NewTicker(b) select { case <-ticker.C: - // Backoff delay is over, stop the ticker and retry. + // Backoff delay is over, stop the ticker and proceed to the next retry attempt. ticker.Stop() case <-ctx.Done(): - // Context is done, stop the ticker and return the context's error. + // If the context is done, stop the ticker and return the context's error. ticker.Stop() err = ctx.Err()