mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-09 23:59:45 +00:00
80 lines
2.1 KiB
Go
80 lines
2.1 KiB
Go
package retry
|
|
|
|
import (
|
|
"math"
|
|
"math/rand"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// Backoff intervals: 1s → 2s → 4s → 8s → 10s (max)
|
|
initialDelay = 1 * time.Second
|
|
maxDelay = 10 * time.Second
|
|
jitterPct = 0.1 // 10% jitter
|
|
// maxAttempt caps the exponent to prevent math.Pow overflow
|
|
// 2^30 seconds is ~34 years, well above maxDelay, so this is safe
|
|
maxAttempt = 30
|
|
)
|
|
|
|
// Backoff implements exponential backoff with jitter for retry logic.
|
|
// The backoff sequence is: 1s → 2s → 4s → 8s → 10s (max, then stays at 10s).
|
|
type Backoff struct {
|
|
attempt int
|
|
rng *rand.Rand
|
|
}
|
|
|
|
// NewBackoff creates a new Backoff instance with a seeded random number generator.
|
|
func NewBackoff() *Backoff {
|
|
return &Backoff{
|
|
attempt: 0,
|
|
// #nosec G404 -- math/rand is appropriate for backoff jitter; cryptographic randomness not needed
|
|
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
|
|
}
|
|
}
|
|
|
|
// Next returns the next backoff duration and increments the attempt counter.
|
|
// The duration follows exponential backoff: 1s → 2s → 4s → 8s → 10s (max).
|
|
// A 10% jitter is added to prevent thundering herd effects.
|
|
func (b *Backoff) Next() time.Duration {
|
|
// Cap attempt to prevent overflow in math.Pow
|
|
attempt := b.attempt
|
|
if attempt > maxAttempt {
|
|
attempt = maxAttempt
|
|
}
|
|
|
|
// Calculate base delay: 2^attempt seconds
|
|
exp := math.Pow(2, float64(attempt))
|
|
delay := time.Duration(exp) * time.Second
|
|
|
|
// Cap at max delay
|
|
if delay > maxDelay {
|
|
delay = maxDelay
|
|
}
|
|
|
|
// Add jitter (±10%)
|
|
jitter := b.calculateJitter(delay)
|
|
delay = delay + jitter
|
|
|
|
b.attempt++
|
|
return delay
|
|
}
|
|
|
|
// Reset resets the backoff to the initial state.
|
|
func (b *Backoff) Reset() {
|
|
b.attempt = 0
|
|
}
|
|
|
|
// Attempt returns the current attempt number.
|
|
func (b *Backoff) Attempt() int {
|
|
return b.attempt
|
|
}
|
|
|
|
// calculateJitter adds random jitter to prevent synchronized retries.
|
|
// Returns a value between -jitterPct*delay and +jitterPct*delay.
|
|
func (b *Backoff) calculateJitter(delay time.Duration) time.Duration {
|
|
maxJitter := float64(delay) * jitterPct
|
|
// Generate random value in range [-maxJitter, +maxJitter]
|
|
jitter := (b.rng.Float64()*2 - 1) * maxJitter
|
|
return time.Duration(jitter)
|
|
}
|