Retry
func Retry[T any](ctx context.Context, attempts int, fn AsyncFunc[T]) (T, error)Retry calls fn until it returns a nil error, it has been called attempts times, or ctx is done.
It runs fn on your goroutine and allocates nothing.
cfg, err := gollback.Retry(ctx, 5, func(ctx context.Context) (Config, error) { return store.Load(ctx, "app")})Attempts
Section titled “Attempts”attempts is the most times fn is called, the first call included. Below 1 means no limit:
Retry calls fn until it succeeds or ctx is done, so give it a context with a deadline.
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)defer cancel()
conn, err := gollback.Retry(ctx, 0, dial) // as many attempts as fit in 30 secondsErrors
Section titled “Errors”- When
fnnever succeeds, Retry returns its last error. - When
ctxis done first, Retry returnsctx’s error joined withfn’s last error, so you can see both why it stopped and why the attempts failed.errors.Is(err, context.DeadlineExceeded)is true. - When
ctxis done before the first call,fnis never called and the error isctx’s alone.
Retry checks ctx before every call, and passes it to fn, which should return early once it is done.
Waiting between attempts
Section titled “Waiting between attempts”Retry calls fn again as soon as it fails. That suits a function that waits by itself, such as one that blocks
on a lock or a connection, but most retries against a service should back off.
Wait at the start of every attempt but the first, and stop waiting when the context is done:
// Command retry fetches a URL, retrying with a growing pause between attempts.//// go run ./examples/retry https://example.compackage main
import ( "context" "fmt" "io" "log" "net/http" "os" "time"
"github.com/vardius/gollback/v2")
// getWithBackoff makes up to attempts requests. Before each retry it waits, doubling the pause every time,// and stops waiting as soon as ctx is done.func getWithBackoff(ctx context.Context, client *http.Client, url string, attempts int, pause time.Duration) (string, error) { var last error // the previous request's return gollback.Retry(ctx, attempts, func(ctx context.Context) (string, error) { if last != nil { select { case <-time.After(pause): pause *= 2 case <-ctx.Done(): // Retry joins ctx's error with this one, so the reason for retrying isn't lost. return "", last } }
body, err := get(ctx, client, url) last = err return body, err })}
func get(ctx context.Context, client *http.Client, url string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", err } resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("%s: %s", url, resp.Status) } body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return "", fmt.Errorf("%s: %w", url, err) }
return string(body), nil}
func main() { log.SetFlags(0) if len(os.Args) != 2 { log.Fatal("usage: retry <url>") }
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel()
body, err := getWithBackoff(ctx, http.DefaultClient, os.Args[1], 5, 200*time.Millisecond) if err != nil { log.Fatal(err) } fmt.Print(body)}