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

Allow configuring a range for jitter to be within #17

Merged
merged 1 commit into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions retrier.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import (

var defaultRandom = rand.New(rand.NewSource(time.Now().UnixNano()))

const jitterInterval = 1000 * time.Millisecond
const defaultJitterInterval = 1000 * time.Millisecond

type Retrier struct {
maxAttempts int
attemptCount int
jitter bool
jitterRange jitterRange
forever bool
rand *rand.Rand

Expand All @@ -27,6 +28,8 @@ type Retrier struct {
manualInterval *time.Duration
}

type jitterRange struct{ min, max time.Duration }

type Strategy func(*Retrier) time.Duration

const (
Expand Down Expand Up @@ -119,6 +122,26 @@ func WithStrategy(strategy Strategy, strategyType string) retrierOpt {
func WithJitter() retrierOpt {
return func(r *Retrier) {
r.jitter = true
r.jitterRange = jitterRange{min: 0, max: defaultJitterInterval}
}
}

// WithJitterRange enables jitter as [WithJitter] does, but allows the user to specify the range of the jitter as a
// half-open range [min, max) of time.Duration values. The jitter will be a random value in the range [min, max) added
// to the interval calculated by the retry strategy. The jitter will be recalculated for each retry. Both min and max may
// be negative, but min must be less than max. min and max may both be zero, which is equivalent to disabling jitter.
// If a negative jitter causes a negative interval, the interval will be clamped to zero.
func WithJitterRange(min, max time.Duration) retrierOpt {
if min >= max {
panic("min must be less than max")
}

return func(r *Retrier) {
r.jitter = true
r.jitterRange = jitterRange{
min: min,
max: max,
}
}
}

Expand Down Expand Up @@ -174,7 +197,9 @@ func (r *Retrier) Jitter() time.Duration {
if !r.jitter {
return 0
}
return time.Duration((1.0 - r.rand.Float64()) * float64(jitterInterval))

min, max := float64(r.jitterRange.min), float64(r.jitterRange.max)
return time.Duration(min + (max-min)*rand.Float64())
}

// MarkAttempt increments the attempt count for the retrier. This affects ShouldGiveUp, and also affects the retry interval
Expand Down
41 changes: 38 additions & 3 deletions retrier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,45 @@ func TestNextInterval_ConstantStrategy_WithJitter(t *testing.T) {

for _, interval := range insomniac.sleepIntervals {
assert.Check(t, interval > expected, "interval: %s, expected: %s", interval, expected)
assert.Check(t, cmp.DeepEqual(interval, expected, opt.DurationWithThreshold(jitterInterval)))
assert.Check(t, cmp.DeepEqual(interval, expected, opt.DurationWithThreshold(defaultJitterInterval)))
}
}

func TestNextInterval_ConstantStrategy_WithJitterRange(t *testing.T) {
t.Parallel()

expected := 5 * time.Second
insomniac := newInsomniac()

err := NewRetrier(
WithStrategy(Constant(expected)),
WithJitterRange(-3*time.Second, 3*time.Second),
WithMaxAttempts(1000),
WithSleepFunc(insomniac.sleep),
).Do(func(_ *Retrier) error { return errDummy })
assert.ErrorIs(t, err, errDummy)

for _, interval := range insomniac.sleepIntervals {
assert.Check(t, cmp.DeepEqual(interval, expected, opt.DurationWithThreshold(6*time.Second)))
}
}

func Test_WhenJitterCausesNegativeInterval_ItDoesntWait(t *testing.T) {
t.Parallel()

before := time.Now()
err := NewRetrier(
WithStrategy(Constant(1*time.Second)),
WithJitterRange(-2*time.Second, -1*time.Second),
WithMaxAttempts(500),
// WithSleepFunc(nothing), // This should "really" sleep (for 0 seconds)
).Do(func(_ *Retrier) error { return errDummy })
after := time.Now()
assert.ErrorIs(t, err, errDummy)

assert.Check(t, after.Sub(before) < 1*time.Millisecond)
}

func TestNextInterval_ExponentialStrategy(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -263,7 +298,7 @@ func TestNextInterval_ExponentialStrategy_WithJitter(t *testing.T) {
16 * time.Second,
},
insomniac.sleepIntervals,
opt.DurationWithThreshold(jitterInterval),
opt.DurationWithThreshold(defaultJitterInterval),
)
}

Expand Down Expand Up @@ -390,7 +425,7 @@ func TestNextInterval_ExponentialSubsecondStrategy_WithJitter(t *testing.T) {
13335 * time.Millisecond,
20535 * time.Millisecond,
31622 * time.Millisecond,
}, insomniac.sleepIntervals, opt.DurationWithThreshold(jitterInterval))
}, insomniac.sleepIntervals, opt.DurationWithThreshold(defaultJitterInterval))
}

func TestString_WithFiniteAttemptCount(t *testing.T) {
Expand Down