diff --git a/backoff.go b/backoff.go index f4c833d..79f6375 100644 --- a/backoff.go +++ b/backoff.go @@ -3,9 +3,15 @@ package durex import ( "math" "math/rand" + "sync" "time" ) +var ( + jitterRandMu sync.Mutex + jitterRand = rand.New(rand.NewSource(time.Now().UnixNano())) +) + // BackoffStrategy calculates the delay before retrying a failed command. type BackoffStrategy interface { // NextDelay returns the delay before the next retry attempt. @@ -72,14 +78,31 @@ type JitteredBackoff struct { // NextDelay implements BackoffStrategy. func (b JitteredBackoff) NextDelay(attempt int) time.Duration { + if b.Strategy == nil { + return 0 + } + base := b.Strategy.NextDelay(attempt) if b.JitterRate <= 0 { return base } - jitterRange := float64(base) * b.JitterRate - jitter := (rand.Float64()*2 - 1) * jitterRange // -jitterRange to +jitterRange - return time.Duration(float64(base) + jitter) + jitterRate := b.JitterRate + if jitterRate > 1 { + jitterRate = 1 + } + + jitterRange := float64(base) * jitterRate + jitterRandMu.Lock() + randomValue := jitterRand.Float64() + jitterRandMu.Unlock() + jitter := (randomValue*2 - 1) * jitterRange // -jitterRange to +jitterRange + delay := time.Duration(float64(base) + jitter) + if delay < 0 { + return 0 + } + + return delay } // DefaultExponentialBackoff returns a sensible default exponential backoff. diff --git a/backoff_test.go b/backoff_test.go index 9fc14c4..e637cb2 100644 --- a/backoff_test.go +++ b/backoff_test.go @@ -148,6 +148,30 @@ func TestJitteredBackoff(t *testing.T) { } } +func TestJitteredBackoff_NilStrategy(t *testing.T) { + backoff := durex.JitteredBackoff{JitterRate: 0.5} + + delay := backoff.NextDelay(1) + if delay != 0 { + t.Errorf("Expected 0 with nil strategy, got %v", delay) + } +} + +func TestJitteredBackoff_ClampsNegativeDelay(t *testing.T) { + base := durex.ConstantBackoff{Delay: 100 * time.Millisecond} + backoff := durex.JitteredBackoff{ + Strategy: base, + JitterRate: 10.0, + } + + for i := 0; i < 100; i++ { + delay := backoff.NextDelay(1) + if delay < 0 { + t.Fatalf("Expected non-negative delay, got %v", delay) + } + } +} + func TestJitteredBackoff_ZeroJitter(t *testing.T) { base := durex.ConstantBackoff{Delay: 100 * time.Millisecond} backoff := durex.JitteredBackoff{