Skip to content

Commit

Permalink
Merge branch 'master' into handle-context-timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
willdot committed Aug 4, 2023
2 parents 9b38e2e + fdadb7c commit b94b74c
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 82 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ jobs:
golangci-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: 1.17
go-version: 1.18
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
Expand All @@ -23,15 +23,15 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: [ '1.16', '1.17', '1.18', '1.19' ]
go-version: ['1.18', '1.19', '1.20']
os: [ubuntu-latest, macos-latest, windows-latest]
env:
OS: ${{ matrix.os }}
GOVERSION: ${{ matrix.go-version }}
steps:
- uses: actions/checkout@v3
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
check-latest: true
Expand Down
52 changes: 46 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,36 @@ http get with retry:
return nil
},
)
if err != nil {
// handle error
}

fmt.Println(string(body))

http get with retry with data:

url := "http://example.com"

fmt.Println(body)
body, err := retry.DoWithData(
func() ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return body, nil
},
)
if err != nil {
// handle error
}

fmt.Println(string(body))

[next examples](https://github.com/avast/retry-go/tree/master/examples)

Expand Down Expand Up @@ -94,6 +122,12 @@ BackOffDelay is a DelayType which increases delay between consecutive retries
func Do(retryableFunc RetryableFunc, opts ...Option) error
```

#### func DoWithData

```go
func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error)
```

#### func FixedDelay

```go
Expand Down Expand Up @@ -355,19 +389,17 @@ wait for a set duration for retries.
example of augmenting time.After with a print statement
type struct MyTimer {}
type struct MyTimer {}
func (t *MyTimer) After(d time.Duration) <- chan time.Time {
fmt.Print("Timer called!")
return time.After(d)
}
retry.Do(
retry.Do(
func() error { ... },
retry.WithTimer(&MyTimer{})
)
)
#### func WrapContextErrorWithLastError
Expand Down Expand Up @@ -409,6 +441,14 @@ type RetryableFunc func() error
Function signature of retryable function
#### type RetryableFuncWithData
```go
type RetryableFuncWithData[T any] func() (T, error)
```
Function signature of retryable function with data
#### type Timer
```go
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.3.3
4.4.0
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
module github.com/avast/retry-go/v4

go 1.13
go 1.18

require github.com/stretchr/testify v1.8.2

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
)
8 changes: 3 additions & 5 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,19 +233,17 @@ func Context(ctx context.Context) Option {
//
// example of augmenting time.After with a print statement
//
// type struct MyTimer {}
// type struct MyTimer {}
//
// func (t *MyTimer) After(d time.Duration) <- chan time.Time {
// fmt.Print("Timer called!")
// return time.After(d)
// }
//
// retry.Do(
//
// retry.Do(
// func() error { ... },
// retry.WithTimer(&MyTimer{})
//
// )
// )
func WithTimer(t Timer) Option {
return func(c *Config) {
c.timer = t
Expand Down
145 changes: 83 additions & 62 deletions retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,36 @@ http get with retry:
return nil
},
)
if err != nil {
// handle error
}
fmt.Println(string(body))
fmt.Println(body)
http get with retry with data:
url := "http://example.com"
body, err := retry.DoWithData(
func() ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
},
)
if err != nil {
// handle error
}
fmt.Println(string(body))
[next examples](https://github.com/avast/retry-go/tree/master/examples)
Expand Down Expand Up @@ -72,6 +100,9 @@ import (
// Function signature of retryable function
type RetryableFunc func() error

// Function signature of retryable function with data
type RetryableFuncWithData[T any] func() (T, error)

// Default timer is a wrapper around time.After
type timerImpl struct{}

Expand All @@ -80,7 +111,17 @@ func (t *timerImpl) After(d time.Duration) <-chan time.Time {
}

func Do(retryableFunc RetryableFunc, opts ...Option) error {
retryableFuncWithData := func() (any, error) {
return nil, retryableFunc()
}

_, err := DoWithData(retryableFuncWithData, opts...)
return err
}

func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error) {
var n uint
var emptyT T

// default
config := newDefaultRetryConfig()
Expand All @@ -91,19 +132,24 @@ func Do(retryableFunc RetryableFunc, opts ...Option) error {
}

if err := config.context.Err(); err != nil {
return err
return emptyT, err
}

// Setting attempts to 0 means we'll retry until we succeed
var lastErr error
if config.attempts == 0 {
for err := retryableFunc(); err != nil; err = retryableFunc() {
for {
t, err := retryableFunc()
if err == nil {
return t, nil
}

if !IsRecoverable(err) {
return err
return emptyT, err
}

if !config.retryIf(err) {
return err
return emptyT, err
}

lastErr = err
Expand All @@ -114,81 +160,66 @@ func Do(retryableFunc RetryableFunc, opts ...Option) error {
case <-config.timer.After(delay(config, n, err)):
case <-config.context.Done():
if config.wrapContextErrorWithLastError {
return Error{config.context.Err(), lastErr}
return emptyT, Error{config.context.Err(), lastErr}
}
return config.context.Err()
return emptyT, config.context.Err()
}
}

return nil
}

var errorLog Error
if !config.lastErrorOnly {
errorLog = make(Error, config.attempts)
} else {
errorLog = make(Error, 1)
}
errorLog := Error{}

attemptsForError := make(map[error]uint, len(config.attemptsForError))
for err, attempts := range config.attemptsForError {
attemptsForError[err] = attempts
}

lastErrIndex := n
shouldRetry := true
for shouldRetry {
err := retryableFunc()
t, err := retryableFunc()
if err == nil {
return t, nil
}

if err != nil {
errorLog[lastErrIndex] = unpackUnrecoverable(err)
errorLog = append(errorLog, unpackUnrecoverable(err))

if !config.retryIf(err) {
break
}
if !config.retryIf(err) {
break
}

config.onRetry(n, err)
config.onRetry(n, err)

for errToCheck, attempts := range attemptsForError {
if errors.Is(err, errToCheck) {
attempts--
attemptsForError[errToCheck] = attempts
shouldRetry = shouldRetry && attempts > 0
}
for errToCheck, attempts := range attemptsForError {
if errors.Is(err, errToCheck) {
attempts--
attemptsForError[errToCheck] = attempts
shouldRetry = shouldRetry && attempts > 0
}
}

// if this is last attempt - don't wait
if n == config.attempts-1 {
break
}
// if this is last attempt - don't wait
if n == config.attempts-1 {
break
}

select {
case <-config.timer.After(delay(config, n, err)):
case <-config.context.Done():
if config.lastErrorOnly {
return config.context.Err()
}
n++
errorLog[n] = config.context.Err()
return errorLog[:lenWithoutNil(errorLog)]
select {
case <-config.timer.After(delay(config, n, err)):
case <-config.context.Done():
if config.lastErrorOnly {
return emptyT, config.context.Err()
}

} else {
return nil
return emptyT, append(errorLog, config.context.Err())
}

n++
shouldRetry = shouldRetry && n < config.attempts

if !config.lastErrorOnly {
lastErrIndex = n
}
}

if config.lastErrorOnly {
return errorLog[lastErrIndex]
return emptyT, errorLog.Unwrap()
}
return errorLog[:lenWithoutNil(errorLog)]
return emptyT, errorLog
}

func newDefaultRetryConfig() *Config {
Expand All @@ -212,7 +243,7 @@ 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, lenWithoutNil(e))
logWithNumber := make([]string, len(e))
for i, l := range e {
if l != nil {
logWithNumber[i] = fmt.Sprintf("#%d: %s", i+1, l.Error())
Expand Down Expand Up @@ -256,17 +287,7 @@ When you need to unwrap all errors, you should use `WrappedErrors()` instead.
Added in version 4.2.0.
*/
func (e Error) Unwrap() error {
return e[lenWithoutNil(e)-1]
}

func lenWithoutNil(e Error) (count int) {
for _, v := range e {
if v != nil {
count++
}
}

return
return e[len(e)-1]
}

// WrappedErrors returns the list of errors that this Error is wrapping.
Expand Down
Loading

0 comments on commit b94b74c

Please sign in to comment.