diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index daab423..a84794d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,7 @@ jobs: name: Test runs-on: ubuntu-latest strategy: + fail-fast: false matrix: go: - "stable" diff --git a/README.md b/README.md index cec694f..93f6142 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,40 @@ func main() { } ``` +```go +package main + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/shogo82148/go-retry" +) + +type Result int + +func DoSomething(ctx context.Context) (Result, error) { + // do something here that should to do exponential backoff https://en.wikipedia.org/wiki/Exponential_backoff + return 0, errors.New("fails") +} + +var policy = retry.Policy{ + MinDelay: 100 * time.Millisecond, + MaxDelay: time.Second, + MaxCount: 10, +} + +func DoSomethingWithRetry(ctx context.Context) (Result, error) { + return retry.DoValue(ctx, policy, DoSomething) +} + +func main() { + fmt.Println(DoSomethingWithRetry(context.Background())) +} +``` + ## PRIOR ARTS This package is based on [lestrrat-go/backoff](https://github.com/lestrrat-go/backoff) and [Yak Shaving With Backoff Libraries in Go](https://medium.com/@lestrrat/yak-shaving-with-backoff-libraries-in-go-80240f0aa30c). diff --git a/example_test.go b/example_test.go index 54c2a40..91b5c1b 100644 --- a/example_test.go +++ b/example_test.go @@ -75,3 +75,25 @@ func ExampleMarkPermanent() { // unstable func is called! // some error! } + +func ExampleDoValue() { + policy := &retry.Policy{ + MaxCount: 3, + } + + count := 0 + _, err := retry.DoValue(context.Background(), policy, func() (int, error) { + count++ + fmt.Printf("#%d: unstable func is called!\n", count) + return 0, errors.New("some error!") + }) + if err != nil { + fmt.Println(err) + } + + // Output: + // #1: unstable func is called! + // #2: unstable func is called! + // #3: unstable func is called! + // some error! +} diff --git a/func.go b/func.go new file mode 100644 index 0000000..9941ee3 --- /dev/null +++ b/func.go @@ -0,0 +1,45 @@ +package retry + +import ( + "context" + "errors" +) + +// DoValue executes f with retrying policy. +// It is a shorthand of Policy.Start and Retrier.Continue. +// If f returns an error, retry to execute f until f returns nil error. +// If the error implements interface{ Temporary() bool } and Temporary() returns false, +// DoValue doesn't retry and returns the error. +func DoValue[T any](ctx context.Context, policy *Policy, f func() (T, error)) (T, error) { + var zero T + var err error + var target *temporary + + retrier := policy.Start(ctx) + for retrier.Continue() { + var v T + v, err = f() + if err == nil { + return v, nil + } + + // short cut for calling isPermanent and Unwrap + if err, ok := err.(*permanentError); ok { + return zero, err.error + } + + if target == nil { + // lazy allocation of target + target = new(temporary) + } + if errors.As(err, target) { + if !(*target).Temporary() { + return zero, err + } + } + } + if err := retrier.err; err != nil { + return zero, err + } + return zero, err +} diff --git a/func_test.go b/func_test.go new file mode 100644 index 0000000..7c98ddf --- /dev/null +++ b/func_test.go @@ -0,0 +1,40 @@ +package retry + +import ( + "context" + "errors" + "fmt" + "testing" +) + +func TestDoValue_Success(t *testing.T) { + policy := &Policy{ + MaxCount: -1, + } + + var count int + v, err := DoValue(context.Background(), policy, func() (int, error) { + count++ + if count < 3 { + return 0, fmt.Errorf("error %d", count) + } + return 42, nil + }) + if err != nil { + t.Fatal(err) + } + if v != 42 { + t.Errorf("want %d, got %d", 42, v) + } +} + +func TestDoValue_MarkPermanent(t *testing.T) { + permanentErr := errors.New("permanent error") + policy := &Policy{} + _, err := DoValue(context.Background(), policy, func() (int, error) { + return 0, MarkPermanent(permanentErr) + }) + if err != permanentErr { + t.Errorf("want error is %#v, got %#v", err, permanentErr) + } +} diff --git a/retry.go b/retry.go index a40e0df..cea71da 100644 --- a/retry.go +++ b/retry.go @@ -60,11 +60,8 @@ func (p *Policy) Start(ctx context.Context) *Retrier { // If the error implements interface{ Temporary() bool } and Temporary() returns false, // Do doesn't retry and returns the error. func (p *Policy) Do(ctx context.Context, f func() error) error { - type Temporary interface { - Temporary() bool - } var err error - var target *Temporary + var target *temporary retrier := p.Start(ctx) for retrier.Continue() { @@ -80,7 +77,7 @@ func (p *Policy) Do(ctx context.Context, f func() error) error { if target == nil { // lazy allocation of target - target = new(Temporary) + target = new(temporary) } if errors.As(err, target) { if !(*target).Temporary() { @@ -94,6 +91,12 @@ func (p *Policy) Do(ctx context.Context, f func() error) error { return err } +type temporary interface { + Temporary() bool +} + +var _ temporary = (*permanentError)(nil) + type permanentError struct { error }