From 3a72194de3cde79b3daf95b91bf3bbf74a141233 Mon Sep 17 00:00:00 2001 From: Janko Simonovic Date: Sat, 14 Feb 2026 21:58:23 +0100 Subject: [PATCH 1/2] Fix jittered backoff edge cases --- backoff.go | 22 ++++++++++++++++++++-- backoff_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/backoff.go b/backoff.go index f4c833d..be015dd 100644 --- a/backoff.go +++ b/backoff.go @@ -6,6 +6,10 @@ import ( "time" ) +func init() { + rand.Seed(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 +76,28 @@ 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 + jitterRate := b.JitterRate + if jitterRate > 1 { + jitterRate = 1 + } + + jitterRange := float64(base) * jitterRate jitter := (rand.Float64()*2 - 1) * jitterRange // -jitterRange to +jitterRange - return time.Duration(float64(base) + jitter) + 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{ From 6971636c26c7b862b619b418cedc32b981372b91 Mon Sep 17 00:00:00 2001 From: Janko Simonovic Date: Sat, 14 Feb 2026 22:05:51 +0100 Subject: [PATCH 2/2] Fix jittered backoff edge cases --- backoff.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/backoff.go b/backoff.go index be015dd..79f6375 100644 --- a/backoff.go +++ b/backoff.go @@ -3,12 +3,14 @@ package durex import ( "math" "math/rand" + "sync" "time" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} +var ( + jitterRandMu sync.Mutex + jitterRand = rand.New(rand.NewSource(time.Now().UnixNano())) +) // BackoffStrategy calculates the delay before retrying a failed command. type BackoffStrategy interface { @@ -91,7 +93,10 @@ func (b JitteredBackoff) NextDelay(attempt int) time.Duration { } jitterRange := float64(base) * jitterRate - jitter := (rand.Float64()*2 - 1) * jitterRange // -jitterRange to +jitterRange + 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