Builds off of the wonderful work of https://github.com/sethvargo/go-retry but adds additional functionality:
Retry is a Go library for facilitating retry logic and backoff strategies. It builds off the work of go-retry and adds additional functionality, such as infinite retries until an error occurs and the ability to reset backoff durations. The library is highly extensible, allowing you to implement custom backoff strategies and integrate them seamlessly into your applications.
-
Infinite Retries Until Error - Repeat will continually do whatever is in the RepeatFunc until it returns an error. It does not observe RetryableError, as there is no need for this functionality.
-
Backoff Reset - You might want to reset a backoff of non-constant duration (e.g., if something happens that says you should poll faster, like a worker having a job available).
-
Extensible - Inspired by Go's built-in HTTP package, this Go backoff and retry library is extensible via middleware. You can write custom backoff functions or use a provided filter.
-
Independent - No external dependencies besides the Go standard library, meaning it won't bloat your project.
-
Concurrent - Unless otherwise specified, everything is safe for concurrent use.
In addition to your own custom algorithms, there are built-in algorithms for backoff in the library.
Retries at a constant interval.
Example:
1s -> 1s -> 1s -> 1s -> 1s -> 1s -> 1s
Usage:
NewConstant(1 * time.Second)
Retries with exponentially increasing intervals.
Arguably the most common backoff, the next value is double the previous value.
Example:
1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s
Usage:
NewExponential(1 * time.Second)
Retries with intervals following the Fibonacci sequence.
The next value is the sum of the current value and the previous value. This means retires happen quickly at first, but then gradually take slower, ideal for network-type issues.
Example:
1s -> 2s -> 3s -> 5s -> 8s -> 13s
Usage:
NewFibonacci(1 * time.Second)
The built-in backoff algorithms never terminate and have no caps or limits - you control their behavior with middleware. There's built-in middleware, but you can also write custom middleware.
Adds randomness to the backoff intervals to prevent thundering herd problems.
To reduce the chances of a thundering herd, add random jitter to the returned value.
backoff, err := NewFibonacci(1 * time.Second)
// Return the next value, +/- 500ms
backoffWithJitter, err := WithJitter(500*time.Millisecond, backoff)
// Return the next value, +/- 5% of the result
backoffWithJitterPercent, err := WithJitterPercent(5, backoff)
Limits the maximum duration between retries.
To ensure an individual calculated duration never exceeds a value, use a cap:
backoff, err := NewFibonacci(1 * time.Second)
// Ensure the maximum value is 2s. In this example, the sleep values would be
// 1s, 1s, 2s, 2s, 2s, 2s...
backoffWithCap := WithCappedDuration(2 * time.Second, backoff)
Limits the maximum total time a backoff should execute.
For a best-effort limit on the total execution time, specify a max duration:
backoff, err := NewFibonacci(1 * time.Second)
// Ensure the maximum total retry time is 5s.
backoffWithMaxDuration = WithMaxDuration(5 * time.Second, backoff)
Limits the number of retry attempts.
To terminate a retry, specify the maximum number of retries. Note this is retries, not attempts. Attempts is retries + 1.
backoff, err := NewFibonacci(1 * time.Second)
// Stop after 4 retries, when the 5th attempt has failed. In this example, the worst case elapsed
// time would be 1s + 1s + 2s + 3s = 7s.
backoffWithMaxRetries = WithMaxRetries(4, backoff)
Stops the backoff if the provided context is Done.
backoff, err := NewFibonacci(1 * time.Second)
ctx, cancel := context.WithTimeout(context.Background, 1 * time.Millisecond)
// backoff will return stop == true when context is cancelled
backoffWithContext := WithContext(ctx, backoff)
To install the library, use the following command:
go get github.com/swayne275/go-retry
This will retry the provided function until it either succeeds or returns a non-retryable error.
package main
import (
"context"
"fmt"
"time"
"github.com/swayne275/go-retry"
"github.com/swayne275/go-retry/backoff"
)
func main() {
ctx := context.Background()
backoff := backoff.NewConstant(1 * time.Second)
err := retry.Do(ctx, backoff, func(ctx context.Context) error {
// Your retryable function logic here
return nil
})
if err != nil {
fmt.Printf("Operation failed: %v\n", err)
}
}
This will repeat the function until it returns a non-retryable error.
package main
import (
"context"
"fmt"
"github.com/swayne275/go-retry/repeat"
)
func main() {
ctx := context.Background()
backoff := backoff.NewExponential(1 * time.Second)
err := repeat.Do(ctx, backoff, func(ctx context.Context) bool {
// Your function logic here - return false to stop repeating
return true
})
if err != nil {
// you can check why the repeat stopped with errors.Is() and the defined
// types in the repeat package
fmt.Printf("Operation failed: %v\n", err)
}
}
package main
import (
"context"
"fmt"
"time"
"github.com/swayne275/go-retry"
"github.com/swayne275/go-retry/backoff"
)
func main() {
ctx := context.Background()
backoff := backoff.NewExponential(1 * time.Second)
resetFunc := func() Backoff {
// define how the backoff shoudl be reset, likely something like:
newB, err := NewExponential(base)
if err != nil {
t.Fatalf("failed to reset exponential backoff: %v", err)
}
return newB
}
backoffWithReset = backoff.WithReset(resetFunc, backoff)
err := retry.Do(ctx, backoffWithReset, func(ctx context.Context) error {
// Your retryable function logic here
// something happens that makes you want to reset back to a shorter backoff
b.Reset()
return nil
})
if err != nil {
fmt.Printf("Operation failed: %v\n", err)
}
}
package main
import (
"context"
"database/sql"
"log"
"time"
"github.com/swayne275/go-retry"
)
func main() {
db, err := sql.Open("mysql", "...")
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
if err := retry.FibonacciRetry(ctx, 1*time.Second, func(ctx context.Context) error {
if err := db.PingContext(ctx); err != nil {
// This marks the error as retryable
return retry.RetryableError(err)
}
return nil
}); err != nil {
log.Fatal(err)
}
}
Here are benchmarks against some other popular Go backoff and retry libraries. You can run these benchmarks yourself via the benchmark/
folder. Commas and spacing fixed for clarity.
swayne275 is roughly in line with sethvargo, which is what it's based off of.
cpu: Apple M3 Max
Benchmark/cenkalti-16 36881959 32.40 ns/op 0 B/op 0 allocs/op
Benchmark/lestrrat-16 1990288 612.5 ns/op 128 B/op 2 allocs/op
Benchmark/sethvargo-16 345455916 3.538 ns/op 0 B/op 0 allocs/op
Benchmark/swayne275-16 339995433 3.510 ns/op 0 B/op 0 allocs/op
- Randomization uses
math/rand
seeded with the Unix timestamp instead ofcrypto/rand
. - Ordering of addition of multiple modifiers will make a difference. For example; ensure you add
CappedDuration
beforeWithMaxDuration
, otherwise it may bail out too early. Another example is you could addJitter
before or after capping depending on your desired outcome.