mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
212 lines
5.7 KiB
212 lines
5.7 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: MPL-2.0 |
|
|
|
package retry |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"math" |
|
"testing" |
|
"time" |
|
|
|
"github.com/stretchr/testify/require" |
|
) |
|
|
|
func TestJitter(t *testing.T) { |
|
repeat(t, "0 percent", func(t *testing.T) { |
|
jitter := NewJitter(0) |
|
for i := 0; i < 10; i++ { |
|
baseTime := time.Duration(i) * time.Second |
|
require.Equal(t, baseTime, jitter(baseTime)) |
|
} |
|
}) |
|
|
|
repeat(t, "10 percent", func(t *testing.T) { |
|
jitter := NewJitter(10) |
|
baseTime := 5000 * time.Millisecond |
|
maxTime := 5500 * time.Millisecond |
|
newTime := jitter(baseTime) |
|
require.True(t, newTime > baseTime) |
|
require.True(t, newTime <= maxTime) |
|
}) |
|
|
|
repeat(t, "100 percent", func(t *testing.T) { |
|
jitter := NewJitter(100) |
|
baseTime := 1234 * time.Millisecond |
|
maxTime := 2468 * time.Millisecond |
|
newTime := jitter(baseTime) |
|
require.True(t, newTime > baseTime) |
|
require.True(t, newTime <= maxTime) |
|
}) |
|
|
|
repeat(t, "overflow", func(t *testing.T) { |
|
jitter := NewJitter(100) |
|
baseTime := time.Duration(math.MaxInt64) - 2*time.Hour |
|
newTime := jitter(baseTime) |
|
require.Equal(t, baseTime, newTime) |
|
}) |
|
} |
|
|
|
func repeat(t *testing.T, name string, fn func(t *testing.T)) { |
|
t.Run(name, func(t *testing.T) { |
|
for i := 0; i < 1000; i++ { |
|
fn(t) |
|
} |
|
}) |
|
} |
|
|
|
func TestWaiter_Delay(t *testing.T) { |
|
t.Run("zero value", func(t *testing.T) { |
|
w := &Waiter{} |
|
for i, expected := range []time.Duration{0, 1, 2, 4, 8, 16, 32, 64, 128} { |
|
w.failures = uint(i) |
|
require.Equal(t, expected*time.Second, w.delay(), "failure count: %d", i) |
|
} |
|
}) |
|
|
|
t.Run("with minimum wait", func(t *testing.T) { |
|
w := &Waiter{MinWait: 5 * time.Second} |
|
for i, expected := range []time.Duration{5, 5, 5, 5, 8, 16, 32, 64, 128} { |
|
w.failures = uint(i) |
|
require.Equal(t, expected*time.Second, w.delay(), "failure count: %d", i) |
|
} |
|
}) |
|
|
|
t.Run("with maximum wait", func(t *testing.T) { |
|
w := &Waiter{MaxWait: 20 * time.Second} |
|
for i, expected := range []time.Duration{0, 1, 2, 4, 8, 16, 20, 20, 20} { |
|
w.failures = uint(i) |
|
require.Equal(t, expected*time.Second, w.delay(), "failure count: %d", i) |
|
} |
|
}) |
|
|
|
t.Run("with minimum failures", func(t *testing.T) { |
|
w := &Waiter{MinFailures: 4} |
|
for i, expected := range []time.Duration{0, 0, 0, 0, 0, 1, 2, 4, 8, 16} { |
|
w.failures = uint(i) |
|
require.Equal(t, expected*time.Second, w.delay(), "failure count: %d", i) |
|
} |
|
}) |
|
|
|
t.Run("with factor", func(t *testing.T) { |
|
w := &Waiter{Factor: time.Millisecond} |
|
for i, expected := range []time.Duration{0, 1, 2, 4, 8, 16, 32, 64, 128} { |
|
w.failures = uint(i) |
|
require.Equal(t, expected*time.Millisecond, w.delay(), "failure count: %d", i) |
|
} |
|
}) |
|
|
|
t.Run("with all settings", func(t *testing.T) { |
|
w := &Waiter{ |
|
MinFailures: 2, |
|
MinWait: 4 * time.Millisecond, |
|
MaxWait: 20 * time.Millisecond, |
|
Factor: time.Millisecond, |
|
} |
|
for i, expected := range []time.Duration{4, 4, 4, 4, 4, 4, 8, 16, 20, 20, 20} { |
|
w.failures = uint(i) |
|
require.Equal(t, expected*time.Millisecond, w.delay(), "failure count: %d", i) |
|
} |
|
}) |
|
|
|
t.Run("jitter can exceed MaxWait", func(t *testing.T) { |
|
w := &Waiter{ |
|
MaxWait: 20 * time.Second, |
|
Jitter: NewJitter(300), |
|
|
|
failures: 30, |
|
} |
|
|
|
delay := w.delay() |
|
require.True(t, delay > 20*time.Second, "expected delay %v to be greater than MaxWait %v", delay, w.MaxWait) |
|
}) |
|
} |
|
|
|
func TestWaiter_Wait(t *testing.T) { |
|
if testing.Short() { |
|
t.Skip("too slow for testing.Short") |
|
} |
|
|
|
ctx := context.Background() |
|
|
|
t.Run("first failure", func(t *testing.T) { |
|
w := &Waiter{MinWait: time.Millisecond, Factor: 1} |
|
elapsed, err := runWait(ctx, w) |
|
require.NoError(t, err) |
|
assertApproximateDuration(t, elapsed, time.Millisecond) |
|
require.Equal(t, w.failures, uint(1)) |
|
}) |
|
|
|
t.Run("max failures", func(t *testing.T) { |
|
w := &Waiter{ |
|
MaxWait: 100 * time.Millisecond, |
|
failures: 200, |
|
} |
|
elapsed, err := runWait(ctx, w) |
|
require.NoError(t, err) |
|
assertApproximateDuration(t, elapsed, 100*time.Millisecond) |
|
require.Equal(t, w.failures, uint(201)) |
|
}) |
|
|
|
t.Run("context deadline", func(t *testing.T) { |
|
w := &Waiter{failures: 200, MinWait: time.Second} |
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Millisecond) |
|
t.Cleanup(cancel) |
|
|
|
elapsed, err := runWait(ctx, w) |
|
require.Equal(t, err, context.DeadlineExceeded) |
|
assertApproximateDuration(t, elapsed, 5*time.Millisecond) |
|
require.Equal(t, w.failures, uint(201)) |
|
}) |
|
} |
|
|
|
func TestWaiter_RetryLoop(t *testing.T) { |
|
if testing.Short() { |
|
t.Skip("too slow for testing.Short") |
|
} |
|
// Change the default factor so that we retry faster. |
|
w := &Waiter{Factor: 1 * time.Millisecond} |
|
|
|
t.Run("exits if operation is successful after a few reties", func(t *testing.T) { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
t.Cleanup(cancel) |
|
numRetries := 0 |
|
err := w.RetryLoop(ctx, func() error { |
|
if numRetries < 2 { |
|
numRetries++ |
|
return fmt.Errorf("operation not successful") |
|
} |
|
return nil |
|
}) |
|
require.NoError(t, err) |
|
}) |
|
|
|
t.Run("errors if operation is never successful", func(t *testing.T) { |
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) |
|
t.Cleanup(cancel) |
|
err := w.RetryLoop(ctx, func() error { |
|
return fmt.Errorf("operation not successful") |
|
}) |
|
require.NotNil(t, err) |
|
require.EqualError(t, err, "could not retry operation: operation not successful") |
|
}) |
|
} |
|
|
|
func runWait(ctx context.Context, w *Waiter) (time.Duration, error) { |
|
before := time.Now() |
|
err := w.Wait(ctx) |
|
return time.Since(before), err |
|
} |
|
|
|
func assertApproximateDuration(t *testing.T, actual time.Duration, expected time.Duration) { |
|
t.Helper() |
|
delta := 20 * time.Millisecond |
|
min, max := expected-delta, expected+delta |
|
if min < 0 { |
|
min = 0 |
|
} |
|
if actual < min || actual > max { |
|
t.Fatalf("expected %v to be between %v and %v", actual, min, max) |
|
} |
|
}
|
|
|