From 72bc10371c529595b55ee43edd6080da0912de04 Mon Sep 17 00:00:00 2001 From: jo Date: Thu, 13 Jun 2024 17:44:12 +0200 Subject: [PATCH] feat: cap exponential backoff to 24s and add jitter https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ --- hcloud/client.go | 26 +++++++++++++++++++++----- hcloud/client_test.go | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/hcloud/client.go b/hcloud/client.go index e433fe329..8a81925d3 100644 --- a/hcloud/client.go +++ b/hcloud/client.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "math" + "math/rand" "net/http" "net/url" "strconv" @@ -40,13 +41,28 @@ func ConstantBackoff(d time.Duration) BackoffFunc { } // ExponentialBackoff returns a BackoffFunc which implements an exponential -// backoff. -// It uses the formula: +// backoff, capped to 24 seconds with an added 50% of jitter. +// See [ExponentialBackoffWithOpts] for more details. +func ExponentialBackoff(multiplier float64, baseDuration time.Duration) BackoffFunc { + return ExponentialBackoffWithOpts(multiplier, baseDuration, 24*time.Second, 0.5) +} + +// ExponentialBackoffWithOpts returns a BackoffFunc which implements an exponential +// backoff, capped to a provided maximum, with an additional jitter ratio. // -// b^retries * d -func ExponentialBackoff(b float64, d time.Duration) BackoffFunc { +// It uses the formula: +// - backoff = min(capDuration, baseDuration * (multiplier ^ retries)) +// - backoff = backoff + (rand(0, backoff) * jitterRatio). +func ExponentialBackoffWithOpts(multiplier float64, baseDuration time.Duration, capDuration time.Duration, jitterRatio float64) BackoffFunc { + baseSeconds := baseDuration.Seconds() + capSeconds := capDuration.Seconds() + return func(retries int) time.Duration { - return time.Duration(math.Pow(b, float64(retries))) * d + backoff := baseSeconds * math.Pow(multiplier, float64(retries)) // Exponential backoff + backoff = math.Min(capSeconds, backoff) // Cap backoff + backoff += rand.Float64() * backoff * jitterRatio // #nosec G404 Add jitter + + return time.Duration(backoff * float64(time.Second)) } } diff --git a/hcloud/client_test.go b/hcloud/client_test.go index 92f478de6..616a148b5 100644 --- a/hcloud/client_test.go +++ b/hcloud/client_test.go @@ -12,6 +12,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/hetznercloud/hcloud-go/v2/hcloud/schema" ) @@ -378,3 +380,20 @@ func TestBuildUserAgent(t *testing.T) { }) } } + +func TestExponentialBackoff(t *testing.T) { + // Turning off jitter for testing + backoffFunc := ExponentialBackoffWithOpts(2, 500*time.Millisecond, 24*time.Second, 0.0) + + count := 8 + sum := 0.0 + result := make([]string, 0, count) + for i := 0; i < count; i++ { + backoff := backoffFunc(i) + sum += backoff.Seconds() + result = append(result, backoff.String()) + } + + require.Equal(t, []string{"500ms", "1s", "2s", "4s", "8s", "16s", "24s", "24s"}, result) + require.Equal(t, 79.5, sum) +}