Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reorganize, test, add repeat functionality, etc #1

Merged
merged 9 commits into from
Jun 30, 2024
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@ Builds off of the wonderful work of https://github.com/sethvargo/go-retry but ad
TODO:
- update godoc to my version
- update this documentation with changes
- put unit tests in same package?
- update Int63n to not panic?
- figure out if we can delete stuff (eg Constant() and the TODOs around that sort of function)
- potentially update the import to be like go-backoff
- split up into better package isolation
- setup github actions to run tests?
- tune up repeat.go to be useful
- add tests around stuff in repeat.go
- perhaps have repeat and retry be separate packages so each can have a Do method?
- add tests to rand.go
- make rand its own package?

## Added features

Expand Down
38 changes: 16 additions & 22 deletions backoff.go → backoff/backoff.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
package retry
package backoff

import (
"sync"
"time"
)

// Backoff is an interface that backs off.
type Backoff interface {
// Next returns the time duration to wait and whether to stop.
Next() (next time.Duration, stop bool)
// Reset sets the undecorated backoff back to its initial parameters
Reset()
}
"github.com/swayne275/go-retry/common/backoff"
)

// TODO clean up interface, struct, etc

var _ Backoff = (BackoffFunc)(nil)
var _ backoff.Backoff = (BackoffFunc)(nil)

// BackoffFunc is a backoff expressed as a function.
type BackoffFunc func() (time.Duration, bool)
Expand All @@ -28,7 +22,7 @@ func (b BackoffFunc) Next() (time.Duration, bool) {
func (b BackoffFunc) Reset() {}

type ResettableBackoff struct {
Backoff
backoff.Backoff
// reset returns the backoff to its initial state.
reset func()
}
Expand All @@ -41,7 +35,7 @@ func (b *ResettableBackoff) Reset() {
b.reset()
}

func WithReset(reset func() Backoff, next Backoff) *ResettableBackoff {
func WithReset(reset func() backoff.Backoff, next backoff.Backoff) *ResettableBackoff {
rb := &ResettableBackoff{
Backoff: next,
}
Expand All @@ -56,7 +50,7 @@ func WithReset(reset func() Backoff, next Backoff) *ResettableBackoff {
// interpreted as "+/- j". For example, if j were 5 seconds and the backoff
// returned 20s, the value could be between 15 and 25 seconds. The value can
// never be less than 0.
func WithJitter(j time.Duration, next Backoff) *ResettableBackoff {
func WithJitter(j time.Duration, next backoff.Backoff) *ResettableBackoff {
r := newLockedRandom(time.Now().UnixNano())

nextWithJitter := BackoffFunc(func() (time.Duration, bool) {
Expand All @@ -73,7 +67,7 @@ func WithJitter(j time.Duration, next Backoff) *ResettableBackoff {
return val, false
})

reset := func() Backoff {
reset := func() backoff.Backoff {
next.Reset()
return nextWithJitter
}
Expand All @@ -85,7 +79,7 @@ func WithJitter(j time.Duration, next Backoff) *ResettableBackoff {
// percentage. j can be interpreted as "+/- j%". For example, if j were 5 and
// the backoff returned 20s, the value could be between 19 and 21 seconds. The
// value can never be less than 0 or greater than 100.
func WithJitterPercent(j uint64, next Backoff) *ResettableBackoff {
func WithJitterPercent(j uint64, next backoff.Backoff) *ResettableBackoff {
r := newLockedRandom(time.Now().UnixNano())

nextWithJitterPercent := BackoffFunc(func() (time.Duration, bool) {
Expand All @@ -105,7 +99,7 @@ func WithJitterPercent(j uint64, next Backoff) *ResettableBackoff {
return val, false
})

reset := func() Backoff {
reset := func() backoff.Backoff {
next.Reset()
return nextWithJitterPercent
}
Expand All @@ -114,7 +108,7 @@ func WithJitterPercent(j uint64, next Backoff) *ResettableBackoff {
}

// WithMaxRetries executes the backoff function up until the maximum attempts.
func WithMaxRetries(max uint64, next Backoff) *ResettableBackoff {
func WithMaxRetries(max uint64, next backoff.Backoff) *ResettableBackoff {
var l sync.Mutex
var attempt uint64

Expand All @@ -135,7 +129,7 @@ func WithMaxRetries(max uint64, next Backoff) *ResettableBackoff {
return val, false
})

reset := func() Backoff {
reset := func() backoff.Backoff {
l.Lock()
defer l.Unlock()
attempt = 0
Expand All @@ -151,7 +145,7 @@ func WithMaxRetries(max uint64, next Backoff) *ResettableBackoff {
// backoff. This is NOT a total backoff time, but rather a cap on the maximum
// value a backoff can return. Without another middleware, the backoff will
// continue infinitely.
func WithCappedDuration(cap time.Duration, next Backoff) *ResettableBackoff {
func WithCappedDuration(cap time.Duration, next backoff.Backoff) *ResettableBackoff {
nextWithCappedDuration := BackoffFunc(func() (time.Duration, bool) {
val, stop := next.Next()
if stop {
Expand All @@ -164,7 +158,7 @@ func WithCappedDuration(cap time.Duration, next Backoff) *ResettableBackoff {
return val, false
})

reset := func() Backoff {
reset := func() backoff.Backoff {
next.Reset()
return nextWithCappedDuration
}
Expand All @@ -175,7 +169,7 @@ func WithCappedDuration(cap time.Duration, next Backoff) *ResettableBackoff {
// WithMaxDuration sets a maximum on the total amount of time a backoff should
// execute. It's best-effort, and should not be used to guarantee an exact
// amount of time.
func WithMaxDuration(timeout time.Duration, next Backoff) *ResettableBackoff {
func WithMaxDuration(timeout time.Duration, next backoff.Backoff) *ResettableBackoff {
var l sync.RWMutex
start := time.Now()

Expand All @@ -199,7 +193,7 @@ func WithMaxDuration(timeout time.Duration, next Backoff) *ResettableBackoff {
return val, false
})

reset := func() Backoff {
reset := func() backoff.Backoff {
l.Lock()
defer l.Unlock()
start = time.Now()
Expand Down
20 changes: 20 additions & 0 deletions backoff/backoff_constant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package backoff

import (
"fmt"
"time"

"github.com/swayne275/go-retry/common/backoff"
)

// NewConstant creates a new constant backoff using the value t. The wait time
// is the provided constant value. It returns an error if t is not greater than 0.
func NewConstant(t time.Duration) (backoff.Backoff, error) {
if t <= 0 {
return nil, fmt.Errorf("constant backoff must be greater than zero")
}

return BackoffFunc(func() (time.Duration, bool) {
return t, false
}), nil
}
22 changes: 11 additions & 11 deletions backoff_constant_test.go → backoff/backoff_constant_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package retry_test
package backoff

import (
"reflect"
"sort"
"testing"
"time"

"github.com/swayne275/go-retry"
cb "github.com/swayne275/go-retry/common/backoff"
)

func TestConstantBackoff(t *testing.T) {
Expand Down Expand Up @@ -75,7 +75,7 @@ func TestConstantBackoff(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

b, err := retry.NewConstant(tc.base)
b, err := NewConstant(tc.base)
if tc.expectErr && err == nil {
t.Fatal("expected an error")
}
Expand Down Expand Up @@ -113,12 +113,12 @@ func TestConstantBackoff(t *testing.T) {

func TestConstantBackoff_WithReset(t *testing.T) {
expectedDuration := 3 * time.Second
b, err := retry.NewConstant(expectedDuration)
b, err := NewConstant(expectedDuration)
if err != nil {
t.Fatalf("failed to create constant backoff: %v", err)
}

resettableB := retry.WithReset(func() retry.Backoff {
resettableB := WithReset(func() cb.Backoff {
return b
}, b)
resettableB.Reset()
Expand All @@ -133,12 +133,12 @@ func TestConstantBackoff_WithCappedDuration_WithReset(t *testing.T) {
expectedDuration := 3 * time.Second
cappedDuration := 2 * time.Second

b, err := retry.NewConstant(expectedDuration)
b, err := NewConstant(expectedDuration)
if err != nil {
t.Fatalf("failed to create constant backoff: %v", err)
}

resettableB := retry.WithCappedDuration(cappedDuration, b)
resettableB := WithCappedDuration(cappedDuration, b)

val, _ := resettableB.Next()
if val != cappedDuration {
Expand All @@ -156,12 +156,12 @@ func TestConstantBackoff_ExplicitReset(t *testing.T) {
expectedDuration := 3 * time.Second
cappedDuration := 2 * time.Second

b, err := retry.NewConstant(expectedDuration)
b, err := NewConstant(expectedDuration)
if err != nil {
t.Fatalf("failed to create constant backoff: %v", err)
}

resettableB := retry.WithCappedDuration(cappedDuration, b)
resettableB := WithCappedDuration(cappedDuration, b)

val, _ := resettableB.Next()
if val != cappedDuration {
Expand All @@ -171,8 +171,8 @@ func TestConstantBackoff_ExplicitReset(t *testing.T) {
// now we're going to explicitly pass in a reset function that DOES NOT observe the cap,
// and we expect the reset to no longer have the cap

explicitylyResettableB := retry.WithReset(func() retry.Backoff {
b, err := retry.NewConstant(expectedDuration)
explicitylyResettableB := WithReset(func() cb.Backoff {
b, err := NewConstant(expectedDuration)
if err != nil {
t.Fatalf("failed to create constant backoff: %v", err)
}
Expand Down
19 changes: 4 additions & 15 deletions backoff_exponential.go → backoff/backoff_exponential.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,27 @@
package retry
package backoff

import (
"context"
"fmt"
"math"
"sync/atomic"
"time"

"github.com/swayne275/go-retry/common/backoff"
)

type exponentialBackoff struct {
base time.Duration
attempt uint64
}

// Exponential is a wrapper around Retry that uses an exponential backoff. See
// NewExponential.
// TODO is this useful or fine as an example?
func Exponential(ctx context.Context, base time.Duration, f RetryFunc) error {
b, err := NewExponential(base)
if err != nil {
return fmt.Errorf("failed to create exponential backoff: %w", err)
}

return Do(ctx, b, f)
}

// NewExponential creates a new exponential backoff using the starting value of
// base and doubling on each failure (1, 2, 4, 8, 16, 32, 64...), up to max.
//
// Once it overflows, the function constantly returns the maximum time.Duration
// for a 64-bit integer.
//
// It returns an error if the given base is less than zero.
func NewExponential(base time.Duration) (Backoff, error) {
func NewExponential(base time.Duration) (backoff.Backoff, error) {
if base <= 0 {
return nil, fmt.Errorf("base must be greater than 0")
}
Expand Down
20 changes: 10 additions & 10 deletions backoff_exponential_test.go → backoff/backoff_exponential_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package retry_test
package backoff

import (
"math"
Expand All @@ -7,7 +7,7 @@ import (
"testing"
"time"

"github.com/swayne275/go-retry"
cb "github.com/swayne275/go-retry/common/backoff"
)

func TestExponentialBackoff(t *testing.T) {
Expand Down Expand Up @@ -81,7 +81,7 @@ func TestExponentialBackoff(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

b, err := retry.NewExponential(tc.base)
b, err := NewExponential(tc.base)
if tc.expectErr && err == nil {
t.Fatal("expected an error")
}
Expand Down Expand Up @@ -126,13 +126,13 @@ func TestExponentialBackoff_WithReset(t *testing.T) {
8 * time.Second,
}

b, err := retry.NewExponential(base)
b, err := NewExponential(base)
if err != nil {
t.Fatalf("failed to create exponential backoff: %v", err)
}

resettableB := retry.WithReset(func() retry.Backoff {
newB, err := retry.NewExponential(base)
resettableB := WithReset(func() cb.Backoff {
newB, err := NewExponential(base)
if err != nil {
t.Fatalf("failed to reset exponential backoff: %v", err)
}
Expand Down Expand Up @@ -168,12 +168,12 @@ func TestExponentialBackoff_WithCappedDuration_WithReset(t *testing.T) {
4 * time.Second,
}

b, err := retry.NewExponential(base)
b, err := NewExponential(base)
if err != nil {
t.Fatalf("failed to create exponential backoff: %v", err)
}

cappedB := retry.WithCappedDuration(cappedDuration, b)
cappedB := WithCappedDuration(cappedDuration, b)

// test pre reset
for i := 0; i < numRounds; i++ {
Expand All @@ -200,9 +200,9 @@ func TestExponentialBackoff_WithCappedDuration_WithReset(t *testing.T) {
4 * time.Second,
8 * time.Second,
}
resettableB := retry.WithReset(func() retry.Backoff {
resettableB := WithReset(func() cb.Backoff {
// don't set a cap on the explicit reset
newB, err := retry.NewExponential(base)
newB, err := NewExponential(base)
if err != nil {
t.Fatalf("failed to reset exponential backoff: %v", err)
}
Expand Down
Loading
Loading