Files
fiber/addon/retry/exponential_backoff_test.go

171 lines
4.0 KiB
Go

package retry
import (
"crypto/rand"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func Test_ExponentialBackoff_Retry(t *testing.T) {
t.Parallel()
tests := []struct {
expErr error
expBackoff *ExponentialBackoff
f func() error
name string
}{
{
name: "With default values - successful",
expBackoff: NewExponentialBackoff(),
f: func() error {
return nil
},
},
{
name: "Successful function",
expBackoff: &ExponentialBackoff{
InitialInterval: 1 * time.Millisecond,
MaxBackoffTime: 100 * time.Millisecond,
Multiplier: 2.0,
MaxRetryCount: 5,
},
f: func() error {
return nil
},
},
{
name: "Unsuccessful function",
expBackoff: &ExponentialBackoff{
InitialInterval: 2 * time.Millisecond,
MaxBackoffTime: 100 * time.Millisecond,
Multiplier: 2.0,
MaxRetryCount: 5,
},
f: func() error {
return errors.New("failed function")
},
expErr: errors.New("failed function"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.expBackoff.Retry(tt.f)
require.Equal(t, tt.expErr, err)
})
}
}
func Test_ExponentialBackoff_Retry_NoSleepAfterLastAttempt(t *testing.T) {
t.Parallel()
const (
largeInterval = 5 * time.Second // would be used for sleep if bug existed
maxAcceptable = 2 * time.Second // Retry must return well before largeInterval
)
eb := &ExponentialBackoff{
InitialInterval: largeInterval,
MaxBackoffTime: largeInterval * 2,
Multiplier: 2.0,
MaxRetryCount: 1,
}
start := time.Now()
err := eb.Retry(func() error { return errors.New("only attempt") })
elapsed := time.Since(start)
require.Error(t, err)
require.Equal(t, "only attempt", err.Error())
require.Less(t, elapsed, maxAcceptable,
"Retry must not sleep after the last failed attempt; took %v (expected < %v)", elapsed, maxAcceptable)
}
func Test_ExponentialBackoff_Next(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expBackoff *ExponentialBackoff
expNextTimeIntervals []time.Duration
}{
{
name: "With default values",
expBackoff: NewExponentialBackoff(),
expNextTimeIntervals: []time.Duration{
1 * time.Second,
2 * time.Second,
4 * time.Second,
8 * time.Second,
16 * time.Second,
32 * time.Second,
32 * time.Second,
32 * time.Second,
32 * time.Second,
32 * time.Second,
},
},
{
name: "Custom values",
expBackoff: &ExponentialBackoff{
InitialInterval: 2.0 * time.Second,
MaxBackoffTime: 64 * time.Second,
Multiplier: 3.0,
MaxRetryCount: 8,
currentInterval: 2.0 * time.Second,
},
expNextTimeIntervals: []time.Duration{
2 * time.Second,
6 * time.Second,
18 * time.Second,
54 * time.Second,
64 * time.Second,
64 * time.Second,
64 * time.Second,
64 * time.Second,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
for i := range tt.expBackoff.MaxRetryCount {
next := tt.expBackoff.next()
if next < tt.expNextTimeIntervals[i] || next > tt.expNextTimeIntervals[i]+1*time.Second {
t.Errorf("wrong next time:\n"+
"actual:%v\n"+
"expected range:%v-%v\n",
next, tt.expNextTimeIntervals[i], tt.expNextTimeIntervals[i]+1*time.Second)
}
}
})
}
}
func Test_ExponentialBackoff_NextRandFailure(t *testing.T) {
// Backup original reader and restore at the end
original := rand.Reader
defer func() { rand.Reader = original }()
rand.Reader = failingReader{}
expBackoff := &ExponentialBackoff{
InitialInterval: 1 * time.Second,
MaxBackoffTime: 10 * time.Second,
Multiplier: 2,
MaxRetryCount: 3,
currentInterval: 1 * time.Second,
}
next := expBackoff.next()
require.Equal(t, expBackoff.MaxBackoffTime, next)
// currentInterval should not change when random fails
require.Equal(t, 1*time.Second, expBackoff.currentInterval)
}
type failingReader struct{}
func (failingReader) Read(_ []byte) (int, error) { return 0, errors.New("fail") }