Skip to content

Commit

Permalink
Add retry method
Browse files Browse the repository at this point in the history
  • Loading branch information
vardius committed May 14, 2019
1 parent 02da615 commit aa4e2c9
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 24 deletions.
45 changes: 33 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ goarch: amd64
pkg: github.com/vardius/gollback
BenchmarkRace-4 10000000 219 ns/op 0 B/op 0 allocs/op
BenchmarkAll-4 5000000 281 ns/op 40 B/op 1 allocs/op
BenchmarkRetry-4 2000000 579 ns/op 208 B/op 3 allocs/op
PASS
ok github.com/vardius/gollback 10.572s
ok github.com/vardius/gollback 23.742s
```

## Race
Expand Down Expand Up @@ -69,12 +70,6 @@ func main() {
return 3, nil
},
)

fmt.Println(r)
fmt.Println(err)
// Output:
// 3
// <nil>
}
```

Expand Down Expand Up @@ -107,12 +102,38 @@ func main() {
return 3, nil
},
)
}
```

## Retry example
> Retry method retries callback given amount of times until it executes without an error, when retries = 0 it will retry infinitely
```go
package main

import (
"context"
"errors"
"fmt"
"time"

"github.com/vardius/gollback"
)

func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

g := gollback.New(ctx)

// Will retry infinitely until timeouts by context (after 5 seconds)
res, err := g.Retry(0, func(ctx context.Context) (interface{}, error) {
return nil, errors.New("failed")
})

fmt.Println(rs)
fmt.Println(errs)
// Output:
// [1 <nil> 3]
// [<nil> failed <nil>]
// Will retry 5 times or will timeout by context (after 5 seconds)
res, e := g.Retry(5, func(ctx context.Context) (interface{}, error) {
return nil, errors.New("failed")
})
}
```

Expand Down
12 changes: 12 additions & 0 deletions benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gollback

import (
"context"
"errors"
"testing"
)

Expand All @@ -23,6 +24,17 @@ func BenchmarkAll(b *testing.B) {
g.All(cbs...)
}

func BenchmarkRetry(b *testing.B) {
g := New(context.Background())
err := errors.New("failed")

b.ResetTimer()

g.Retry(b.N, func(ctx context.Context) (interface{}, error) {
return nil, err
})
}

func getCallbacks(b *testing.B) []AsyncFunc {
cbs := make([]AsyncFunc, b.N)
for n := 0; n < b.N; n++ {
Expand Down
33 changes: 33 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,36 @@ func Example_all() {
// [1 <nil> 3]
// [<nil> failed <nil>]
}

func Example_retry_timeout() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

g := gollback.New(ctx)

// Will retry infinitely until timeouts by context (after 5 seconds)
res, err := g.Retry(0, func(ctx context.Context) (interface{}, error) {
return nil, errors.New("failed")
})

fmt.Println(res)
fmt.Println(err)
// Output:
// <nil>
// context deadline exceeded
}

func Example_retry_five() {
g := gollback.New(context.Background())

// Will retry 5 times
res, err := g.Retry(5, func(ctx context.Context) (interface{}, error) {
return nil, errors.New("failed")
})

fmt.Println(res)
fmt.Println(err)
// Output:
// <nil>
// failed
}
55 changes: 43 additions & 12 deletions gollback.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,29 @@ package gollback

import (
"context"
"errors"
"sync"
"time"
)

// AsyncFunc represents asynchronous function
type AsyncFunc func(ctx context.Context) (interface{}, error)

// Gollback provides set of utility methods to easily manage asynchronous functions
type Gollback interface {
// Race method returns a response as soon as one of the callbacks in an iterable resolves with the value that is not an error,
// Race method returns a response as soon as one of the callbacks in an iterable executes without an error,
// otherwise last error is returned
Race(fns ...AsyncFunc) (interface{}, error)
// All method returns when all of the callbacks passed as an iterable have finished,
// returned responses and errors are ordered according to callback order
All(fns ...AsyncFunc) ([]interface{}, []error)
// Retry method retries callback given amount of times until it executes without an error,
// when retries = 0 it will retry infinitely
Retry(retries int, fn AsyncFunc) (interface{}, error)
}

type gollback struct {
gollbacks []AsyncFunc
ctx context.Context
cancel context.CancelFunc
ctx context.Context
}

type response struct {
Expand All @@ -30,24 +33,25 @@ type response struct {
}

func (p *gollback) Race(fns ...AsyncFunc) (interface{}, error) {
ctx, cancel := context.WithCancel(p.ctx)
out := make(chan *response, 1)

for i, fn := range fns {
go func(index int, f AsyncFunc) {
for {
select {
case <-p.ctx.Done():
case <-ctx.Done():
return
default:
var r response
r.res, r.err = f(p.ctx)
r.res, r.err = f(ctx)

if p.ctx.Err() != nil {
if ctx.Err() != nil {
return
}

if r.err == nil || index == len(fns)-1 {
p.cancel()
cancel()
out <- &r
}

Expand All @@ -58,6 +62,7 @@ func (p *gollback) Race(fns ...AsyncFunc) (interface{}, error) {
}

r := <-out
cancel()

return r.res, r.err
}
Expand Down Expand Up @@ -86,16 +91,42 @@ func (p *gollback) All(fns ...AsyncFunc) ([]interface{}, []error) {
return rs, errs
}

func (p *gollback) Retry(retires int, fn AsyncFunc) (interface{}, error) {
i := 1

for {
select {
case <-time.After(1 * time.Second):
return nil, errors.New("timeout")
case <-p.ctx.Done():
return nil, p.ctx.Err()
default:
var r response
r.res, r.err = fn(p.ctx)

if r.err == nil || i == retires {
return r.res, r.err
}

i++
}
}
}

func (p *gollback) RetryWithTimeout(retires int, duration time.Duration, fn AsyncFunc) (interface{}, error) {
ctx, cancel := context.WithTimeout(p.ctx, duration)
defer cancel()

return fn(ctx)
}

// New creates new gollback
func New(ctx context.Context) Gollback {
if ctx == nil {
ctx = context.Background()
}

ctx, cancel := context.WithCancel(ctx)

return &gollback{
ctx: ctx,
cancel: cancel,
ctx: ctx,
}
}
42 changes: 42 additions & 0 deletions gollback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,48 @@ func TestAll(t *testing.T) {
}
}

func TestRetryTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

g := New(ctx)

// Will retry infinitely until timeouts by context (after 5 seconds)
_, err := g.Retry(0, func(ctx context.Context) (interface{}, error) {
return nil, errors.New("failed")
})

if err != ctx.Err() {
t.Fail()
}
}

func TestRetryFail(t *testing.T) {
g := New(context.Background())
err := errors.New("failed")

// Will retry 5 times
_, e := g.Retry(5, func(ctx context.Context) (interface{}, error) {
return nil, err
})

if err != e {
t.Fail()
}
}

func TestRetrySuccess(t *testing.T) {
g := New(context.Background())

res, _ := g.Retry(5, func(ctx context.Context) (interface{}, error) {
return "success", nil
})

if res != "success" {
t.Fail()
}
}

func testErrorsEq(a, b []error) bool {
if (a == nil) != (b == nil) {
return false
Expand Down

0 comments on commit aa4e2c9

Please sign in to comment.