From 9284b81ef02e7bbfa92b72e89efcfb2d4871e580 Mon Sep 17 00:00:00 2001 From: Victor Toni Date: Fri, 19 Apr 2024 15:25:06 +0200 Subject: [PATCH] Add exponential backoff implementation and tests --- autopaho/backoff.go | 119 +++++++++++++++++++++++++++++++++++++++ autopaho/backoff_test.go | 74 ++++++++++++++++++++++++ 2 files changed, 193 insertions(+) diff --git a/autopaho/backoff.go b/autopaho/backoff.go index b2ec0ad..acb5dea 100644 --- a/autopaho/backoff.go +++ b/autopaho/backoff.go @@ -16,6 +16,7 @@ package autopaho import ( + "math/rand" "time" ) @@ -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, + } +} diff --git a/autopaho/backoff_test.go b/autopaho/backoff_test.go index 9c9478e..23d93c3 100644 --- a/autopaho/backoff_test.go +++ b/autopaho/backoff_test.go @@ -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 +}