diff --git a/rehttp.go b/rehttp.go index 3b9f6b4..c3fa747 100644 --- a/rehttp.go +++ b/rehttp.go @@ -64,6 +64,11 @@ import ( // PRNG is the *math.Rand value to use to add jitter to the backoff // algorithm used in ExpJitterDelay. By default it uses a *rand.Rand // initialized with a source based on the current time in nanoseconds. +// +// Deprecated: math/rand sources can panic if used concurrently without +// synchronization. PRNG is no longer used by this package and its use +// outside this package is discouraged. +// https://github.com/PuerkitoBio/rehttp/issues/12 var PRNG = rand.New(rand.NewSource(time.Now().UnixNano())) // terribly named interface to detect errors that support Temporary. @@ -264,7 +269,24 @@ func ExpJitterDelay(base, max time.Duration) DelayFn { exp := math.Pow(2, float64(attempt.Index)) top := float64(base) * exp return time.Duration( - PRNG.Int63n(int64(math.Min(float64(max), top))), + rand.Int63n(int64(math.Min(float64(max), top))), + ) + } +} + +// ExpJitterDelayWithRand returns a DelayFn that returns a delay +// between 0 and base * 2^attempt capped at max (an exponential +// backoff delay with jitter). The provided generator will be used +// in place of rand.Int63n. +// +// See the full jitter algorithm in: +// http://www.awsarchitectureblog.com/2015/03/backoff.html +func ExpJitterDelayWithRand(base, max time.Duration, generator func(n int64) int64) DelayFn { + return func(attempt Attempt) time.Duration { + exp := math.Pow(2, float64(attempt.Index)) + top := float64(base) * exp + return time.Duration( + generator(int64(math.Min(float64(max), top))), ) } } diff --git a/rehttp_delayfn_test.go b/rehttp_delayfn_test.go index bebc4c8..1f3c1e4 100644 --- a/rehttp_delayfn_test.go +++ b/rehttp_delayfn_test.go @@ -27,3 +27,14 @@ func TestExpJitterDelay(t *testing.T) { assert.True(t, delay <= actual, "%d: %s > %s", i, delay, actual) } } + +func TestExpJitterDelayWithRand(t *testing.T) { + fn := ExpJitterDelayWithRand(time.Second, 5*time.Second, func(n int64) int64 { return 999_999_999 % n }) + for i := 0; i < 10; i++ { + delay := fn(Attempt{Index: i}) + top := math.Pow(2, float64(i)) * float64(time.Second) + actual := time.Duration(math.Min(float64(5*time.Second), top)) + assert.True(t, delay > 0, "%d: %s <= 0", i, delay) + assert.True(t, delay <= actual, "%d: %s > %s", i, delay, actual) + } +} diff --git a/rng_pre120.go b/rng_pre120.go new file mode 100644 index 0000000..8362d50 --- /dev/null +++ b/rng_pre120.go @@ -0,0 +1,15 @@ +//go:build !go1.20 +// +build !go1.20 + +package rehttp + +import ( + "math/rand" + "time" +) + +// Only seed the global random source on versions prior to 1.20 since +// it's pre-seeded starting in 1.20 +func init() { + rand.Seed(time.Now().UnixNano()) +}