-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add v2 implementation of repeater with strategies and tests
- Implement retry strategies: FixedDelay and Backoff with types (Constant, Linear, Exponential) and options for max delay and jitter. - Add Repeater struct to handle retry logic, providing NewBackoff and NewFixed constructor functions. - Include error handling with immediate termination on specified critical errors, including ErrAny for stopping on any error. - Integrate full testing suite to confirm correct behavior of retry mechanisms, including context cancellation respects. - Update README with usage examples and detailed explanations of strategies and options. - Configure GitHub Actions CI for building and testing Go v1.24 codebase with coverage reporting.
- Loading branch information
Showing
8 changed files
with
776 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
name: build-v2 | ||
|
||
on: | ||
push: | ||
branches: | ||
tags: | ||
paths: | ||
- ".github/workflows/ci-v2.yml" | ||
- "v2/**" | ||
pull_request: | ||
paths: | ||
- ".github/workflows/ci-v2.yml" | ||
- "v2/**" | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: set up go | ||
uses: actions/setup-go@v5 | ||
with: | ||
go-version: "1.24" | ||
id: go | ||
|
||
- name: checkout | ||
uses: actions/checkout@v4 | ||
|
||
- name: build and test | ||
run: | | ||
go test -timeout=60s -v -race -p 1 -covermode=atomic -coverprofile=$GITHUB_WORKSPACE/profile.cov ./... | ||
go build -race | ||
working-directory: v2 | ||
|
||
- name: golangci-lint | ||
uses: golangci/golangci-lint-action@v4 | ||
with: | ||
version: latest | ||
args: --config ../.golangci.yml | ||
working-directory: v2 | ||
|
||
- name: submit coverage | ||
run: | | ||
go install github.com/mattn/goveralls@latest | ||
goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov | ||
env: | ||
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
# Repeater | ||
|
||
[](https://github.com/go-pkgz/repeater/actions) [](https://goreportcard.com/report/github.com/go-pkgz/repeater) [](https://coveralls.io/github/go-pkgz/repeater?branch=master) | ||
|
||
Package repeater implements a functional mechanism to repeat operations with different retry strategies. | ||
|
||
## Install and update | ||
|
||
`go get -u github.com/go-pkgz/repeater/v2` | ||
|
||
## Usage | ||
|
||
### Basic Example with Exponential Backoff | ||
|
||
```go | ||
// create repeater with exponential backoff | ||
r := repeater.NewBackoff(5, time.Second) // 5 attempts starting with 1s delay | ||
|
||
err := r.Do(ctx, func() error { | ||
// do something that may fail | ||
return nil | ||
}) | ||
``` | ||
|
||
### Fixed Delay with Critical Error | ||
|
||
```go | ||
// create repeater with fixed delay | ||
r := repeater.NewFixed(3, 100*time.Millisecond) | ||
|
||
criticalErr := errors.New("critical error") | ||
|
||
err := r.Do(ctx, func() error { | ||
// do something that may fail | ||
return fmt.Errorf("temp error") | ||
}, criticalErr) // will stop immediately if criticalErr returned | ||
``` | ||
|
||
### Custom Backoff Strategy | ||
|
||
```go | ||
r := repeater.NewBackoff(5, time.Second, | ||
repeater.WithMaxDelay(10*time.Second), | ||
repeater.WithBackoffType(repeater.BackoffLinear), | ||
repeater.WithJitter(0.1), | ||
) | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
defer cancel() | ||
|
||
err := r.Do(ctx, func() error { | ||
// do something that may fail | ||
return nil | ||
}) | ||
``` | ||
|
||
### Stop on Any Error | ||
|
||
```go | ||
r := repeater.NewFixed(3, time.Millisecond) | ||
|
||
err := r.Do(ctx, func() error { | ||
return errors.New("some error") | ||
}, repeater.ErrAny) // will stop on any error | ||
``` | ||
|
||
## Strategies | ||
|
||
The package provides several retry strategies: | ||
|
||
1. **Fixed Delay** - each retry happens after a fixed time interval | ||
2. **Backoff** - delay between retries increases according to the chosen algorithm: | ||
- Constant - same delay between attempts | ||
- Linear - delay increases linearly | ||
- Exponential - delay doubles with each attempt | ||
|
||
Backoff strategy can be customized with: | ||
- Maximum delay cap | ||
- Jitter to prevent thundering herd | ||
- Different backoff types (constant/linear/exponential) | ||
|
||
### Custom Strategies | ||
|
||
You can implement your own retry strategy by implementing the Strategy interface: | ||
|
||
```go | ||
type Strategy interface { | ||
// NextDelay returns delay for the next attempt | ||
// attempt starts from 1 | ||
NextDelay(attempt int) time.Duration | ||
} | ||
``` | ||
|
||
Example of a custom strategy that increases delay by a custom factor: | ||
|
||
```go | ||
// CustomStrategy implements Strategy with custom factor-based delays | ||
type CustomStrategy struct { | ||
Initial time.Duration | ||
Factor float64 | ||
} | ||
|
||
func (s CustomStrategy) NextDelay(attempt int) time.Duration { | ||
if attempt <= 0 { | ||
return 0 | ||
} | ||
delay := time.Duration(float64(s.Initial) * math.Pow(s.Factor, float64(attempt-1))) | ||
return delay | ||
} | ||
|
||
// Usage | ||
strategy := &CustomStrategy{Initial: time.Second, Factor: 1.5} | ||
r := repeater.NewWithStrategy(5, strategy) | ||
err := r.Do(ctx, func() error { | ||
// attempts will be delayed by: 1s, 1.5s, 2.25s, 3.37s, 5.06s | ||
return nil | ||
}) | ||
``` | ||
|
||
## Options | ||
|
||
For backoff strategy, several options are available: | ||
|
||
```go | ||
WithMaxDelay(time.Duration) // set maximum delay between retries | ||
WithBackoffType(BackoffType) // set backoff type (constant/linear/exponential) | ||
WithJitter(float64) // add randomness to delays (0-1.0) | ||
``` | ||
|
||
## Error Handling | ||
|
||
- Stops on context cancellation | ||
- Can stop on specific errors (pass them as additional parameters to Do) | ||
- Special `ErrAny` to stop on any error | ||
- Returns last error if all attempts fail |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
module github.com/go-pkgz/repeater/v2 | ||
|
||
go 1.24 | ||
|
||
require github.com/stretchr/testify v1.10.0 | ||
|
||
require ( | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package repeater | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"time" | ||
) | ||
|
||
// ErrAny is a special sentinel error that, when passed as a critical error to Do, | ||
// makes it fail on any error from the function | ||
var ErrAny = errors.New("any error") | ||
|
||
// Repeater holds configuration for retry operations | ||
type Repeater struct { | ||
strategy Strategy | ||
attempts int | ||
} | ||
|
||
// NewWithStrategy creates a repeater with a custom retry strategy | ||
func NewWithStrategy(attempts int, strategy Strategy) *Repeater { | ||
if attempts <= 0 { | ||
attempts = 1 | ||
} | ||
if strategy == nil { | ||
strategy = NewFixedDelay(time.Second) | ||
} | ||
return &Repeater{ | ||
attempts: attempts, | ||
strategy: strategy, | ||
} | ||
} | ||
|
||
// NewBackoff creates a repeater with backoff strategy | ||
// Default settings (can be overridden with options): | ||
// - 30s max delay | ||
// - exponential backoff | ||
// - 10% jitter | ||
func NewBackoff(attempts int, initial time.Duration, opts ...backoffOption) *Repeater { | ||
return NewWithStrategy(attempts, newBackoff(initial, opts...)) | ||
} | ||
|
||
// NewFixed creates a repeater with fixed delay strategy | ||
func NewFixed(attempts int, delay time.Duration) *Repeater { | ||
return NewWithStrategy(attempts, NewFixedDelay(delay)) | ||
} | ||
|
||
// Do repeats fun until it succeeds or max attempts reached | ||
// terminates immediately on context cancellation or if err matches any in termErrs. | ||
// if errs contains ErrAny, terminates on any error. | ||
func (r *Repeater) Do(ctx context.Context, fun func() error, termErrs ...error) error { | ||
var lastErr error | ||
|
||
inErrors := func(err error) bool { | ||
for _, e := range termErrs { | ||
if errors.Is(e, ErrAny) { | ||
return true | ||
} | ||
if errors.Is(err, e) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
for attempt := 0; attempt < r.attempts; attempt++ { | ||
// check context before each attempt | ||
if err := ctx.Err(); err != nil { | ||
return err | ||
} | ||
|
||
var err error | ||
if err = fun(); err == nil { | ||
return nil | ||
} | ||
|
||
lastErr = err | ||
if inErrors(err) { | ||
return err | ||
} | ||
|
||
// don't sleep after the last attempt | ||
if attempt < r.attempts-1 { | ||
delay := r.strategy.NextDelay(attempt + 1) | ||
if delay > 0 { | ||
select { | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
case <-time.After(delay): | ||
} | ||
} | ||
} | ||
} | ||
|
||
return lastErr | ||
} |
Oops, something went wrong.