Skip to content

Commit

Permalink
feat(retier): Add WithNotifier option
Browse files Browse the repository at this point in the history
`WithNotier` option sets a callback function that gets triggered
on each retry attempt, providing feedback on errors and backoff.
  • Loading branch information
enenumxela committed Nov 13, 2024
1 parent ef8a550 commit 73f1e3d
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 14 deletions.
3 changes: 2 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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

Expand Down
40 changes: 40 additions & 0 deletions examples/basic/main.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
50 changes: 46 additions & 4 deletions retrier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
25 changes: 16 additions & 9 deletions retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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():
Expand All @@ -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()
Expand Down

0 comments on commit 73f1e3d

Please sign in to comment.