Files
kportal/internal/retry/backoff.go
T
2025-12-14 18:17:20 +00:00

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)
}