Files
kportal/internal/retry/backoff.go
T
lukaszraczylo 96ae1d45e0 style: Extract UI constants and refactor main view rendering (#30)
- [x] Add golangci-lint configuration with gocritic ifElseChain disabled
- [x] Rename error variables to avoid shadowing (createErr, watcherErr, watchErr, etc.)
- [x] Replace `interface{}` with `any` type alias throughout codebase
- [x] Add package-level documentation comments to all internal packages
- [x] Reorder struct fields alphabetically for consistency
- [x] Extract UI constants (terminal dimensions, column widths, colors) to constants.go
- [x] Refactor BubbleTeaUI main view rendering into smaller helper functions
- [x] Simplify nested conditionals and improve code clarity
- [x] Add `isForwardDisabled()` helper method to BubbleTeaUI
- [x] Update file permissions from 0644 to 0600 in config tests
- [x] Add `#nosec` comments and error suppression where appropriate
- [x] Improve test table struct field ordering for readability
- [x] Fix resource parsing in AddForward using strings.SplitN
- [x] Add comprehensive tests for new UI helper functions and constants
2026-01-13 09:37:45 +00:00

96 lines
2.6 KiB
Go

// Package retry provides exponential backoff with jitter for retry logic.
// It implements a backoff sequence of 1s → 2s → 4s → 8s → 10s (max),
// with 10% random jitter to prevent thundering herd problems.
//
// Basic usage:
//
// backoff := retry.NewBackoff()
// for {
// err := doSomething()
// if err == nil {
// backoff.Reset()
// break
// }
// delay := backoff.Next()
// time.Sleep(delay)
// }
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 {
rng *rand.Rand
attempt int
}
// 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 += 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)
}