Skip to content

Commit

Permalink
v5
Browse files Browse the repository at this point in the history
  • Loading branch information
cenkalti committed Dec 16, 2024
1 parent 66b23f9 commit 3d3869e
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 470 deletions.
62 changes: 0 additions & 62 deletions context.go

This file was deleted.

25 changes: 0 additions & 25 deletions context_test.go

This file was deleted.

24 changes: 24 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package backoff

// PermanentError signals that the operation should not be retried.
type PermanentError struct {
Err error
}

// Permanent wraps the given err in a *PermanentError.
func Permanent(err error) error {
if err == nil {
return nil
}
return &PermanentError{
Err: err,
}
}

func (e *PermanentError) Error() string {
return e.Err.Error()
}

func (e *PermanentError) Unwrap() error {
return e.Err
}
26 changes: 3 additions & 23 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,11 @@ import (

func ExampleRetry() {
// An operation that may fail.
operation := func() error {
return nil // or an error
operation := func() (bool, error) {
return true, nil
}

err := Retry(operation, NewExponentialBackOff())
if err != nil {
// Handle error.
return
}

// Operation is successful.
}

func ExampleRetryContext() { // nolint: govet
// A context
ctx := context.Background()

// An operation that may fail.
operation := func() error {
return nil // or an error
}

b := WithContext(NewExponentialBackOff(), ctx)

err := Retry(operation, b)
_, err := Retry(context.TODO(), operation, WithBackOff(NewExponentialBackOff()))
if err != nil {
// Handle error.
return
Expand Down
96 changes: 6 additions & 90 deletions exponential.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,131 +56,47 @@ type ExponentialBackOff struct {
RandomizationFactor float64
Multiplier float64
MaxInterval time.Duration
// After MaxElapsedTime the ExponentialBackOff stops.
// It never stops if MaxElapsedTime == 0.
MaxElapsedTime time.Duration
Clock Clock

currentInterval time.Duration
startTime time.Time
}

// Clock is an interface that returns current time for BackOff.
type Clock interface {
Now() time.Time
}

// ExponentialBackOffOpts is a function type used to configure ExponentialBackOff options.
type ExponentialBackOffOpts func(*ExponentialBackOff)

// Default values for ExponentialBackOff.
const (
DefaultInitialInterval = 500 * time.Millisecond
DefaultRandomizationFactor = 0.5
DefaultMultiplier = 1.5
DefaultMaxInterval = 60 * time.Second
DefaultMaxElapsedTime = 15 * time.Minute
)

// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
func NewExponentialBackOff(opts ...ExponentialBackOffOpts) *ExponentialBackOff {
b := &ExponentialBackOff{
func NewExponentialBackOff() *ExponentialBackOff {
return &ExponentialBackOff{
InitialInterval: DefaultInitialInterval,
RandomizationFactor: DefaultRandomizationFactor,
Multiplier: DefaultMultiplier,
MaxInterval: DefaultMaxInterval,
MaxElapsedTime: DefaultMaxElapsedTime,
Clock: SystemClock,
}
for _, fn := range opts {
fn(b)
}
b.Reset()
return b
}

// WithInitialInterval sets the initial interval between retries.
func WithInitialInterval(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.InitialInterval = duration
}
}

// WithRandomizationFactor sets the randomization factor to add jitter to intervals.
func WithRandomizationFactor(randomizationFactor float64) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.RandomizationFactor = randomizationFactor
}
}

// WithMultiplier sets the multiplier for increasing the interval after each retry.
func WithMultiplier(multiplier float64) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Multiplier = multiplier
}
}

// WithMaxInterval sets the maximum interval between retries.
func WithMaxInterval(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.MaxInterval = duration
}
}

// WithMaxElapsedTime sets the maximum total time for retries.
func WithMaxElapsedTime(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.MaxElapsedTime = duration
}
}

// WithClockProvider sets the clock used to measure time.
func WithClockProvider(clock Clock) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Clock = clock
}
}

type systemClock struct{}

func (t systemClock) Now() time.Time {
return time.Now()
}

// SystemClock implements Clock interface that uses time.Now().
var SystemClock = systemClock{}

// Reset the interval back to the initial retry interval and restarts the timer.
// Reset must be called before using b.
func (b *ExponentialBackOff) Reset() {
b.currentInterval = b.InitialInterval
b.startTime = b.Clock.Now()
}

// NextBackOff calculates the next backoff interval using the formula:
//
// Randomized interval = RetryInterval * (1 ± RandomizationFactor)
func (b *ExponentialBackOff) NextBackOff() time.Duration {
// Make sure we have not gone over the maximum elapsed time.
elapsed := b.GetElapsedTime()
if b.currentInterval == 0 {
b.currentInterval = b.InitialInterval
}

next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval)
b.incrementCurrentInterval()
if b.MaxElapsedTime != 0 && elapsed+next > b.MaxElapsedTime {
return Stop
}
return next
}

// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance
// is created and is reset when Reset() is called.
//
// The elapsed time is computed using time.Now().UnixNano(). It is
// safe to call even while the backoff policy is used by a running
// ticker.
func (b *ExponentialBackOff) GetElapsedTime() time.Duration {
return b.Clock.Now().Sub(b.startTime)
}

// Increments the current interval by multiplying it with the multiplier.
func (b *ExponentialBackOff) incrementCurrentInterval() {
// Check for overflow, if overflow is detected set the current interval to the max interval.
Expand Down
70 changes: 0 additions & 70 deletions exponential_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ func TestBackOff(t *testing.T) {
testRandomizationFactor = 0.1
testMultiplier = 2.0
testMaxInterval = 5 * time.Second
testMaxElapsedTime = 15 * time.Minute
)

exp := NewExponentialBackOff()
exp.InitialInterval = testInitialInterval
exp.RandomizationFactor = testRandomizationFactor
exp.Multiplier = testMultiplier
exp.MaxInterval = testMaxInterval
exp.MaxElapsedTime = testMaxElapsedTime
exp.Reset()

var expectedResults = []time.Duration{500, 1000, 2000, 4000, 5000, 5000, 5000, 5000, 5000, 5000}
Expand Down Expand Up @@ -52,37 +50,6 @@ func TestGetRandomizedInterval(t *testing.T) {
assertEquals(t, 3, getRandomValueFromInterval(0.5, 0.99, 2))
}

type TestClock struct {
i time.Duration
start time.Time
}

func (c *TestClock) Now() time.Time {
t := c.start.Add(c.i)
c.i += time.Second
return t
}

func TestGetElapsedTime(t *testing.T) {
var exp = NewExponentialBackOff()
exp.Clock = &TestClock{}
exp.Reset()

var elapsedTime = exp.GetElapsedTime()
if elapsedTime != time.Second {
t.Errorf("elapsedTime=%d", elapsedTime)
}
}

func TestMaxElapsedTime(t *testing.T) {
var exp = NewExponentialBackOff()
exp.Clock = &TestClock{start: time.Time{}.Add(10000 * time.Second)}
// Change the currentElapsedTime to be 0 ensuring that the elapsed time will be greater
// than the max elapsed time.
exp.startTime = time.Time{}
assertEquals(t, Stop, exp.NextBackOff())
}

func TestBackOffOverflow(t *testing.T) {
var (
testInitialInterval time.Duration = math.MaxInt64 / 2
Expand All @@ -106,40 +73,3 @@ func assertEquals(t *testing.T, expected, value time.Duration) {
t.Errorf("got: %d, expected: %d", value, expected)
}
}

func TestNewExponentialBackOff(t *testing.T) {
// Create a new ExponentialBackOff with custom options
backOff := NewExponentialBackOff(
WithInitialInterval(1*time.Second),
WithMultiplier(2.0),
WithMaxInterval(10*time.Second),
WithMaxElapsedTime(30*time.Second),
WithClockProvider(SystemClock),
)

// Check that the backOff object is not nil
if backOff == nil {
t.Error("Expected a non-nil ExponentialBackOff object, got nil")
}

// Check that the custom options were applied correctly
if backOff.InitialInterval != 1*time.Second {
t.Errorf("Expected InitialInterval to be 1 second, got %v", backOff.InitialInterval)
}

if backOff.Multiplier != 2.0 {
t.Errorf("Expected Multiplier to be 2.0, got %v", backOff.Multiplier)
}

if backOff.MaxInterval != 10*time.Second {
t.Errorf("Expected MaxInterval to be 10 seconds, got %v", backOff.MaxInterval)
}

if backOff.MaxElapsedTime != 30*time.Second {
t.Errorf("Expected MaxElapsedTime to be 30 seconds, got %v", backOff.MaxElapsedTime)
}

if backOff.Clock != SystemClock {
t.Errorf("Expected Clock to be SystemClock, got %v", backOff.Clock)
}
}
Loading

0 comments on commit 3d3869e

Please sign in to comment.