A simple exponential backoff counter in Go (Golang)
$ go get -v github.com/jpillora/backoff
Backoff is a time.Duration
counter. It starts at Min
. After every call to Duration()
it is multiplied by Factor
. It is capped at Max
. It returns to Min
on every call to Reset()
. Jitter
adds randomness (see below). Used in conjunction with the time
package.
For a higher-level API, see Retry.
https://pkg.go.dev/github.com/jpillora/backoff
b := &backoff.Backoff{
//These are the defaults
Min: 100 * time.Millisecond,
Max: 10 * time.Second,
Factor: 2,
Jitter: false,
}
fmt.Printf("%s\n", b.Duration())
fmt.Printf("%s\n", b.Duration())
fmt.Printf("%s\n", b.Duration())
fmt.Printf("Reset!\n")
b.Reset()
fmt.Printf("%s\n", b.Duration())
100ms
200ms
400ms
Reset!
100ms
b := &backoff.Backoff{
Max: 5 * time.Minute,
}
for {
conn, err := net.Dial("tcp", "example.com:5309")
if err != nil {
d := b.Duration()
fmt.Printf("%s, reconnecting in %s", err, d)
time.Sleep(d)
continue
}
//connected
b.Reset()
conn.Write([]byte("hello world!"))
// ... Read ... Write ... etc
conn.Close()
//disconnected
}
Enabling Jitter
adds some randomization to the backoff durations. See Amazon's writeup of performance gains using jitter. Seeding is not necessary but doing so gives repeatable results.
import "math/rand"
b := &backoff.Backoff{
Jitter: true,
}
rand.Seed(42)
fmt.Printf("%s\n", b.Duration())
fmt.Printf("%s\n", b.Duration())
fmt.Printf("%s\n", b.Duration())
fmt.Printf("Reset!\n")
b.Reset()
fmt.Printf("%s\n", b.Duration())
fmt.Printf("%s\n", b.Duration())
fmt.Printf("%s\n", b.Duration())
100ms
106.600049ms
281.228155ms
Reset!
100ms
104.381845ms
214.957989ms
The Retry
type extends Backoff
to provide a simpler, higher-level API for implementing exponential backoff/retry logic.
Here's an example of how you can use it:
// retry is reused, to maintain state across calls to `doSomethingWithRetry`
retry := &backoff.Retry{Backoff: &backoff.Backoff{
Min: 100 * time.Millisecond,
Max: 10 * time.Second,
Factor: 2,
}}
// doSomethingWithRetry enforces a rate limit on calling `doSomething`, which
// persists across calls to `doSomethingWithRetry`, resetting on success
doSomethingWithRetry := func(maxAttempts int) (err error) {
for attempt := 0; attempt < maxAttempts; {
if next := retry.Allow(); next != (time.Time{}) {
// if an attempt is not yet allowed, wait until it is
time.Sleep(time.Until(next))
continue
}
attempt++
err = doSomething() // the action being attempted
if err != nil {
// in practice you might have some logging here, or just handle the
// returned (last) error
continue
}
// success!
// allow another call immediately, reset attempts
retry.Reset()
return nil
}
return err
}
// call `doSomethingWithRetry`, whenever you want to `doSomething`
Forked from some JavaScript written by @tj