diff --git a/README.md b/README.md index ace68bb..ef1971c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ An exponentially backing off retry package for Go. [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/coder/retry) ``` -go get github.com/coder/retry +go get github.com/coder/retry@latest ``` ## Features diff --git a/func.go b/func.go new file mode 100644 index 0000000..5608f8c --- /dev/null +++ b/func.go @@ -0,0 +1,32 @@ +package retry + +import "context" + +type abortError struct { + error +} + +// Abort returns an error that will cause the retry loop to immediately abort. +// The underlying error will be returned from the Do method. +func Abort(err error) error { + return abortError{err} +} + +func Func[T any](fn func() (T, error), r *Retrier) func(context.Context) (T, error) { + return func(ctx context.Context) (T, error) { + var ( + v T + err error + ) + for r.Wait(ctx) { + v, err = fn() + if err == nil { + return v, nil + } + if _, ok := err.(abortError); ok { + return v, err + } + } + return v, ctx.Err() + } +} diff --git a/func_test.go b/func_test.go new file mode 100644 index 0000000..70c7135 --- /dev/null +++ b/func_test.go @@ -0,0 +1,28 @@ +package retry + +import ( + "context" + "errors" + "testing" + "time" +) + +func TestFunc(t *testing.T) { + t.Parallel() + + passAfter := time.Now().Add(time.Second) + + dog, err := Func(func() (string, error) { + if time.Now().Before(passAfter) { + return "", errors.New("not yet") + } + return "dog", nil + }, + New(time.Millisecond, time.Millisecond))(context.Background()) + if err != nil { + t.Fatal(err) + } + if dog != "dog" { + t.Fatal("expected dog") + } +} diff --git a/retrier.go b/retrier.go index 007664c..1d8f182 100644 --- a/retrier.go +++ b/retrier.go @@ -21,12 +21,20 @@ func New(floor, ceil time.Duration) *Retrier { } } -func (r *Retrier) Wait(ctx context.Context) bool { +// Next returns the next delay duration without modifying the retry state. +// This is useful for logging. +func (r *Retrier) Next() time.Duration { const growth = 2 - r.delay *= growth - if r.delay > r.ceil { - r.delay = r.ceil + delay := r.delay * growth + if delay > r.ceil { + delay = r.ceil } + return delay +} + +// Wait waits for the next retry and returns true if the retry should be attempted. +func (r *Retrier) Wait(ctx context.Context) bool { + r.delay = r.Next() select { case <-time.After(r.delay): if r.delay < r.floor {