Skip to content

Commit

Permalink
feat: add truncated exponential backoff with full jitter backoff (#459)
Browse files Browse the repository at this point in the history
- Add a truncated exponential back off function with full jitter,
- The existing exponential back off function is now capped to 60s.


https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
  • Loading branch information
jooola authored Jul 10, 2024
1 parent 96d4226 commit fd1f46c
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 5 deletions.
41 changes: 36 additions & 5 deletions hcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"math"
"math/rand"
"net/http"
"net/url"
"strconv"
Expand Down Expand Up @@ -40,13 +41,43 @@ func ConstantBackoff(d time.Duration) BackoffFunc {
}

// ExponentialBackoff returns a BackoffFunc which implements an exponential
// backoff.
// It uses the formula:
// backoff, truncated to 60 seconds.
// See [ExponentialBackoffWithOpts] for more details.
func ExponentialBackoff(multiplier float64, base time.Duration) BackoffFunc {
return ExponentialBackoffWithOpts(ExponentialBackoffOpts{
Base: base,
Multiplier: multiplier,
Cap: time.Minute,
})
}

// ExponentialBackoffOpts defines the options used by [ExponentialBackoffWithOpts].
type ExponentialBackoffOpts struct {
Base time.Duration
Multiplier float64
Cap time.Duration
Jitter bool
}

// ExponentialBackoffWithOpts returns a BackoffFunc which implements an exponential
// backoff, truncated to a maximum, and an optional full jitter.
//
// b^retries * d
func ExponentialBackoff(b float64, d time.Duration) BackoffFunc {
// See https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
func ExponentialBackoffWithOpts(opts ExponentialBackoffOpts) BackoffFunc {
baseSeconds := opts.Base.Seconds()
capSeconds := opts.Cap.Seconds()

return func(retries int) time.Duration {
return time.Duration(math.Pow(b, float64(retries))) * d
// Exponential backoff
backoff := baseSeconds * math.Pow(opts.Multiplier, float64(retries))
// Cap backoff
backoff = math.Min(capSeconds, backoff)
// Add jitter
if opts.Jitter {
backoff = ((backoff - baseSeconds) * rand.Float64()) + baseSeconds // #nosec G404
}

return time.Duration(backoff * float64(time.Second))
}
}

Expand Down
22 changes: 22 additions & 0 deletions hcloud/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)

Expand Down Expand Up @@ -378,3 +380,23 @@ func TestBuildUserAgent(t *testing.T) {
})
}
}

func TestExponentialBackoff(t *testing.T) {
backoffFunc := ExponentialBackoffWithOpts(ExponentialBackoffOpts{
Base: time.Second,
Multiplier: 2,
Cap: 32 * time.Second,
})

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{"1s", "2s", "4s", "8s", "16s", "32s", "32s", "32s"}, result)
require.Equal(t, 127.0, sum)
}

0 comments on commit fd1f46c

Please sign in to comment.