Skip to content

Commit

Permalink
Add exponential backoff implementation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ViToni committed Apr 19, 2024
1 parent e23d980 commit 9284b81
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 0 deletions.
119 changes: 119 additions & 0 deletions autopaho/backoff.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package autopaho

import (
"math/rand"
"time"
)

Expand Down Expand Up @@ -72,3 +73,121 @@ func NewConstantBackoffStrategy(duration time.Duration) *ConstantBackoffStrategy
func (c *ConstantBackoffStrategy) GetBackoff() Backoff {
return c._Backoff
}

////////////////////////////////////////////////////////////////////////////////
// implementation for an exponential backoff strategy
////////////////////////////////////////////////////////////////////////////////

// The ExponentialBackoff implements the Backoff interface and provides a backoff duration that increases exponentially up to a specied max value.
// The backoff duration is computed as a random value between the min and the current max value.
// The current max is updated by multiplying the current max with the factor up the the max value
// Implementaion note: For simplicity the backoff uses numbers instead of duration.
type ExponentialBackoff struct {
Backoff
_Min int64
_Max int64
_MovingMax int64
_Factor float32
}

func NewExponentialBackoff(
min time.Duration,
max time.Duration,
initialMax time.Duration,
factor float32,
) *ExponentialBackoff {
return &ExponentialBackoff{
_Min: min.Milliseconds(),
_Max: max.Milliseconds(),
_MovingMax: initialMax.Milliseconds(),
_Factor: factor,
}
}

func (eb *ExponentialBackoff) Next() time.Duration {
backoff := eb._computeDuration()
defer eb._updateRange()

return backoff
}

// Computes the next backoff duration which will have a random value between the min and the current max value.
func (eb *ExponentialBackoff) _computeDuration() time.Duration {
normalizedRangeInMillis := eb._MovingMax - eb._Min
randomMillisInRange := rand.Int63n(normalizedRangeInMillis) + eb._Min
randomDurationInRange := time.Duration(randomMillisInRange) * time.Millisecond

return randomDurationInRange
}

// Updates the current max value by multiplying it with the factor and does not exceed the configured max value.
func (eb *ExponentialBackoff) _updateRange() {
nextMax := int64(float32(eb._MovingMax) * eb._Factor)

// ensure we stay in range
// overflow of range OR numerical overflow
if eb._Max < nextMax || nextMax < eb._MovingMax {
nextMax = eb._Max
}

eb._MovingMax = nextMax
}

type ExponentialBackoffStrategy struct {
BackoffStrategy
_Min time.Duration
_Max time.Duration
_InitialMax time.Duration
_Factor float32
}

func (ebs *ExponentialBackoffStrategy) GetBackoff() Backoff {
return NewExponentialBackoff(
ebs._Min,
ebs._Max,
ebs._InitialMax,
ebs._Factor,
)
}

func NewExponentialBackoffStrategy(
min time.Duration,
max time.Duration,
initialMax time.Duration,
factor float32,
) *ExponentialBackoffStrategy {
if min <= 0 {
panic("min Duration must not be lower than or equal to: 0")
}
if max <= min {
panic("max Duration must not be less than or equal to min Duration")
}
if initialMax < min || max < initialMax {
panic("initial max Duration must be in range ]min, max[")
}
if factor <= 1 {
panic("factor must not be less than or equal to: 1")
}

return &ExponentialBackoffStrategy{
_Min: min,
_Max: max,
_InitialMax: initialMax,
_Factor: factor,
}
}

// DefaultExponentialBackoffStrategy returns a new ExponentialBackoffStrategy with default values.
// The default values are:
// - min: 5 seconds
// - max: 10 minutes
// - initial max: 10 seconds
// - factor: 1.5
func DefaultExponentialBackoffStrategy() *ExponentialBackoffStrategy {
return &ExponentialBackoffStrategy{
_Min: 5 * time.Second,
_Max: 10 * time.Minute,
_InitialMax: 10 * time.Second,
_Factor: 1.5,
}
}
74 changes: 74 additions & 0 deletions autopaho/backoff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,77 @@ func TestConstantBackoffStrategyRandomValue(t *testing.T) {
}
}
}

// tests for the exponential backoff strategy implementation

func TestRandomExponentialBackoff(t *testing.T) {
for i := 0; i < 20; i++ {
doSetupAndTestRandomExponentialBackoff(t)
}
}

func doSetupAndTestRandomExponentialBackoff(t *testing.T) {
// set up a partially random min backoff time
minBackoffInMillisLowerBound := 500 // 500ms
minBackoffInMillisUpperBound := minBackoffInMillisLowerBound + 5*1_000 // +
minBackoffInMillis := RandInt(
minBackoffInMillisLowerBound,
minBackoffInMillisUpperBound,
)

minBackoff := time.Duration(minBackoffInMillis) * time.Millisecond

// set up a partially random initial max backoff time
initialMaxBackoffInMillisLowerBound := minBackoffInMillis + 500 // +500ms
initialMaxBackoffInMillisUpperBound := initialMaxBackoffInMillisLowerBound + 30*1_000 // +30s
initialMaxBackoffInMillis := RandInt(
initialMaxBackoffInMillisLowerBound,
initialMaxBackoffInMillisUpperBound,
)

initialMaxBackoff := time.Duration(initialMaxBackoffInMillis) * time.Millisecond

// set up a partially random max backoff time
maxBackoffInMillisLowerBound := minBackoffInMillis + 30*60*1_000 // +30min
maxBackoffInMillisUpperBound := maxBackoffInMillisLowerBound + 60*60*1_000 // +60min
maxBackoffInMillis := RandInt(
maxBackoffInMillisLowerBound,
maxBackoffInMillisUpperBound,
)

maxBackoff := time.Duration(maxBackoffInMillis) * time.Millisecond

// set up factor for the next variation
const factor = 2.0

exponentialBackoffStrategy := NewExponentialBackoffStrategy(
minBackoff,
maxBackoff,
initialMaxBackoff,
factor,
)

exponentialBackoff := exponentialBackoffStrategy.GetBackoff()

// create many backof and test they are within constraints
for i := 0; i < 50; i++ {
actual := exponentialBackoff.Next()
if i == 0 {
if initialMaxBackoff < actual {
t.Fatalf("Actual backoff value: `%s` was higher than configured initial maximum: `%s`", actual, initialMaxBackoff)
}
}
if actual < minBackoff {
t.Fatalf("Actual backoff value: `%s` was less than configured minimum: `%s`", actual, minBackoff)
}
if maxBackoff < actual {
t.Fatalf("Actual backoff value: `%s` was higher than configured maximum: `%s`", actual, maxBackoff)
}
}
}

func RandInt(min int, max int) int {
normalizedRange := max - min

return rand.Intn(normalizedRange) + max
}

0 comments on commit 9284b81

Please sign in to comment.