Skip to content

Commit

Permalink
major refactoring (#5)
Browse files Browse the repository at this point in the history
* 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.

* lint: separate linter config and few minor warns

* add ref to v2 to readme

* drop v2 as we pre-v1
  • Loading branch information
umputun authored Feb 15, 2025
1 parent 390ed95 commit 1da8089
Show file tree
Hide file tree
Showing 13 changed files with 658 additions and 513 deletions.
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: set up go 1.20
- name: set up go 1.24
uses: actions/setup-go@v3
with:
go-version: "1.20"
go-version: "1.24"
id: go

- name: checkout
Expand All @@ -26,12 +26,9 @@ jobs:
go test -timeout=60s -race -covermode=atomic -coverprofile=$GITHUB_WORKSPACE/profile.cov_tmp
cat $GITHUB_WORKSPACE/profile.cov_tmp | grep -v "_mock.go" > $GITHUB_WORKSPACE/profile.cov
go build -race
env:
GO111MODULE: "on"
TZ: "America/Chicago"
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v4
with:
version: latest

Expand Down
69 changes: 32 additions & 37 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
linters-settings:
govet:
check-shadowing: true
golint:
min-confidence: 0.6
shadow: true
gocyclo:
min-complexity: 15
maligned:
suggest-new: true
dupl:
threshold: 100
goconst:
min-len: 2
min-occurrences: 2
Expand All @@ -23,61 +19,60 @@ linters-settings:
- experimental
disabled-checks:
- wrapperFunc
- hugeParam
- rangeValCopy

linters:
disable-all: true
enable:
- megacheck
- staticcheck
- revive
- govet
- unconvert
- gas
- misspell
- unused
- gosec
- unparam
- typecheck
- ineffassign
- stylecheck
- gochecknoinits
- exportloopref
- copyloopvar
- gocritic
- nakedret
- gosimple
- prealloc

- unused
- contextcheck
- copyloopvar
- decorder
- errorlint
- exptostd
- gochecknoglobals
- gofmt
- goimports
- nilerr
- predeclared
- testifylint
- thelper
fast: false
disable-all: true


run:
# modules-download-mode: vendor
skip-dirs:
- vendor
concurrency: 4

issues:
exclude-rules:
- text: "should have a package comment, unless it's in another file for this package"
linters:
- golint
- text: "exitAfterDefer:"
linters:
- gocritic
- text: "whyNoLint: include an explanation for nolint directive"
linters:
- gocritic
- text: "go.mongodb.org/mongo-driver/bson/primitive.E"
linters:
- govet
- text: "weak cryptographic primitive"
- text: "G114: Use of net/http serve function that has no support for setting timeouts"
linters:
- gosec
- text: "at least one file in a package should have a package comment"
linters:
- stylecheck
- text: "should have a package comment"
linters:
- linters:
- unparam
- revive
- text: 'Deferring unsafe method "Close" on type "io.ReadCloser"'
linters:
path: _test\.go$
text: "unused-parameter"
- linters:
- prealloc
path: _test\.go$
text: "Consider pre-allocating"
- linters:
- gosec
- intrange
path: _test\.go$
exclude-use-default: false
21 changes: 0 additions & 21 deletions LICENSE

This file was deleted.

134 changes: 118 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,135 @@
# Repeater [![Build Status](https://github.com/go-pkgz/repeater/workflows/build/badge.svg)](https://github.com/go-pkgz/repeater/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/repeater)](https://goreportcard.com/report/github.com/go-pkgz/repeater) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/repeater/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/repeater?branch=master)
# Repeater

Repeater calls a function until it returns no error, up to some number of iterations and delays defined by strategy. It terminates immediately on err from the provided (optional) list of critical errors.
[![Build Status](https://github.com/go-pkgz/repeater/workflows/build/badge.svg)](https://github.com/go-pkgz/repeater/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/repeater)](https://goreportcard.com/report/github.com/go-pkgz/repeater) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/repeater/badge.svg?branch=master)](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`

## How to use
## 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
})
```

New Repeater created by `New(strtg strategy.Interface)` or shortcut for default - `NewDefault(repeats int, delay time.Duration) *Repeater`.
### Fixed Delay with Critical Error

To activate invoke `Do` method. `Do` repeats func until no error returned. Predefined (optional) errors terminate the loop immediately.
`func (r Repeater) Do(ctx context.Context, fun func() error, errors ...error) (err error)`
```go
// create repeater with fixed delay
r := repeater.NewFixed(3, 100*time.Millisecond)

### Repeating strategy
criticalErr := errors.New("critical error")

User can provide his own strategy implementing the interface:
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
type Interface interface {
Start(ctx context.Context) chan struct{}
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
})
```

Returned channels used as "ticks," i.e., for each repeat or initial operation one read from this channel needed. Closing the channel indicates "done with retries." It is pretty much the same idea as `time.Timer` or `time.Tick` implements. Note - the first (technically not-repeated-yet) call won't happen **until something sent to the channel**. For this reason, the typical strategy sends the first "tick" before the first wait/sleep.
## 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)
```

Three strategies provided byt the package:
## Error Handling

1. **Fixed delay**, up to max number of attempts. It is the default strategy used by `repeater.NewDefault` constructor.
2. **BackOff** with jitter provides an exponential backoff. It starts from `Duration` interval and goes in steps with `last * math.Pow(factor, attempt)`. Optional jitter randomizes intervals a little. _Factor = 1 effectively makes this strategy fixed with `Duration` delay._
3. **Once** strategy does not do any repeats and mainly used for tests/mocks`.
- 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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module github.com/go-pkgz/repeater

go 1.20
go 1.24

require github.com/stretchr/testify v1.8.2
require github.com/stretchr/testify v1.10.0

require (
github.com/davecgh/go-spew v1.1.1 // indirect
Expand Down
11 changes: 2 additions & 9 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading

0 comments on commit 1da8089

Please sign in to comment.