Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: cenkalti/backoff
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.1.3
Choose a base ref
...
head repository: cenkalti/backoff
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v4.2.0
Choose a head ref
  • 3 commits
  • 5 files changed
  • 1 contributor

Commits on Nov 19, 2022

  1. Copy the full SHA
    d815f46 View commit details
  2. Added GitHub Actions

    ItalyPaleAle authored and cenkalti committed Nov 19, 2022
    Copy the full SHA
    911f539 View commit details
  3. Remove travis

    ItalyPaleAle authored and cenkalti committed Nov 19, 2022
    Copy the full SHA
    e5c9822 View commit details
Showing with 119 additions and 33 deletions.
  1. +23 −0 .github/workflows/go.yaml
  2. +0 −10 .travis.yml
  3. +1 −1 go.mod
  4. +42 −8 retry.go
  5. +53 −14 retry_test.go
23 changes: 23 additions & 0 deletions .github/workflows/go.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Go

on:
push:
pull_request:

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
10 changes: 0 additions & 10 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/cenkalti/backoff/v4

go 1.13
go 1.18
50 changes: 42 additions & 8 deletions retry.go
Original file line number Diff line number Diff line change
@@ -5,10 +5,20 @@ import (
"time"
)

// An OperationWithData is executing by RetryWithData() or RetryNotifyWithData().
// The operation will be retried using a backoff policy if it returns an error.
type OperationWithData[T any] func() (T, error)

// An Operation is executing by Retry() or RetryNotify().
// The operation will be retried using a backoff policy if it returns an error.
type Operation func() error

func (o Operation) withEmptyData() OperationWithData[struct{}] {
return func() (struct{}, error) {
return struct{}{}, o()
}
}

// Notify is a notify-on-error function. It receives an operation error and
// backoff delay if the operation failed (with an error).
//
@@ -28,18 +38,41 @@ func Retry(o Operation, b BackOff) error {
return RetryNotify(o, b, nil)
}

// RetryWithData is like Retry but returns data in the response too.
func RetryWithData[T any](o OperationWithData[T], b BackOff) (T, error) {
return RetryNotifyWithData(o, b, nil)
}

// RetryNotify calls notify function with the error and wait duration
// for each failed attempt before sleep.
func RetryNotify(operation Operation, b BackOff, notify Notify) error {
return RetryNotifyWithTimer(operation, b, notify, nil)
}

// RetryNotifyWithData is like RetryNotify but returns data in the response too.
func RetryNotifyWithData[T any](operation OperationWithData[T], b BackOff, notify Notify) (T, error) {
return doRetryNotify(operation, b, notify, nil)
}

// RetryNotifyWithTimer calls notify function with the error and wait duration using the given Timer
// for each failed attempt before sleep.
// A default timer that uses system timer is used when nil is passed.
func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error {
var err error
var next time.Duration
_, err := doRetryNotify(operation.withEmptyData(), b, notify, t)
return err
}

// RetryNotifyWithTimerAndData is like RetryNotifyWithTimer but returns data in the response too.
func RetryNotifyWithTimerAndData[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) {
return doRetryNotify(operation, b, notify, t)
}

func doRetryNotify[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) {
var (
err error
next time.Duration
res T
)
if t == nil {
t = &defaultTimer{}
}
@@ -52,21 +85,22 @@ func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer

b.Reset()
for {
if err = operation(); err == nil {
return nil
res, err = operation()
if err == nil {
return res, nil
}

var permanent *PermanentError
if errors.As(err, &permanent) {
return permanent.Err
return res, permanent.Err
}

if next = b.NextBackOff(); next == Stop {
if cerr := ctx.Err(); cerr != nil {
return cerr
return res, cerr
}

return err
return res, err
}

if notify != nil {
@@ -77,7 +111,7 @@ func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer

select {
case <-ctx.Done():
return ctx.Err()
return res, ctx.Err()
case <-t.C():
}
}
67 changes: 53 additions & 14 deletions retry_test.go
Original file line number Diff line number Diff line change
@@ -55,6 +55,36 @@ func TestRetry(t *testing.T) {
}
}

func TestRetryWithData(t *testing.T) {
const successOn = 3
var i = 0

// This function is successful on "successOn" calls.
f := func() (int, error) {
i++
log.Printf("function is called %d. time\n", i)

if i == successOn {
log.Println("OK")
return 42, nil
}

log.Println("error")
return 1, errors.New("error")
}

res, err := RetryNotifyWithTimerAndData(f, NewExponentialBackOff(), nil, &testTimer{})
if err != nil {
t.Errorf("unexpected error: %s", err.Error())
}
if i != successOn {
t.Errorf("invalid number of retries: %d", i)
}
if res != 42 {
t.Errorf("invalid data in response: %d, expected 42", res)
}
}

func TestRetryContext(t *testing.T) {
var cancelOn = 3
var i = 0
@@ -90,15 +120,15 @@ func TestRetryContext(t *testing.T) {
}

func TestRetryPermanent(t *testing.T) {
ensureRetries := func(test string, shouldRetry bool, f func() error) {
ensureRetries := func(test string, shouldRetry bool, f func() (int, error), expectRes int) {
numRetries := -1
maxRetries := 1

_ = RetryNotifyWithTimer(
func() error {
res, _ := RetryNotifyWithTimerAndData(
func() (int, error) {
numRetries++
if numRetries >= maxRetries {
return Permanent(errors.New("forced"))
return -1, Permanent(errors.New("forced"))
}
return f()
},
@@ -114,43 +144,52 @@ func TestRetryPermanent(t *testing.T) {
if !shouldRetry && numRetries > 0 {
t.Errorf("Test: '%s', backoff should not have retried", test)
}

if res != expectRes {
t.Errorf("Test: '%s', got res %d but expected %d", test, res, expectRes)
}
}

for _, testCase := range []struct {
name string
f func() error
f func() (int, error)
shouldRetry bool
res int
}{
{
"nil test",
func() error {
return nil
func() (int, error) {
return 1, nil
},
false,
1,
},
{
"io.EOF",
func() error {
return io.EOF
func() (int, error) {
return 2, io.EOF
},
true,
-1,
},
{
"Permanent(io.EOF)",
func() error {
return Permanent(io.EOF)
func() (int, error) {
return 3, Permanent(io.EOF)
},
false,
3,
},
{
"Wrapped: Permanent(io.EOF)",
func() error {
return fmt.Errorf("Wrapped error: %w", Permanent(io.EOF))
func() (int, error) {
return 4, fmt.Errorf("Wrapped error: %w", Permanent(io.EOF))
},
false,
4,
},
} {
ensureRetries(testCase.name, testCase.shouldRetry, testCase.f)
ensureRetries(testCase.name, testCase.shouldRetry, testCase.f, testCase.res)
}
}