retry

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 16, 2020 License: MIT Imports: 4 Imported by: 18

README

retry GoDoc Build Status Go Report Card Coverage Status

♻️¯\_ʕ◔ϖ◔ʔ_/¯

retry is a simple retrier for golang with exponential backoff and context support.

It exists mainly because I found the other libraries either too heavy in implementation or not to my liking.

retry is simple and opinionated; it re-runs your code with a particular ("full jitter") exponential backoff implementation, it supports context, and it lets you bail early on non-retryable errors. It does not implement constant backoff or alternative jitter schemes.

Retrier objects are intended to be re-used, which means you define them once and then run functions with them whenever you want, as many times as you want. This is safe for concurrent use.

Usage

Simple

// create a new retrier that will try a maximum of five times, with
// an initial delay of 100 ms and a maximum delay of 1 second
retrier := retry.NewRetrier(5, 100 * time.Millisecond, time.Second)

err := retrier.Run(func() error {
    resp, err := http.Get("http://golang.org")
    switch {
    case err != nil:
        // request error - return it
        return err
    case resp.StatusCode == 0 || resp.StatusCode >= 500:
        // retryable StatusCode - return it
        return fmt.Errorf("Retryable HTTP status: %s", http.StatusText(resp.StatusCode))
    case resp.StatusCode != 200:
        // non-retryable error - stop now
        return retry.Stop(fmt.Errorf("Non-retryable HTTP status: %s", http.StatusText(resp.StatusCode)))
    }
    return nil
})
if err != nil {
    // handle error
}

With context

// create a new retrier that will try a maximum of five times, with
// an initial delay of 100 ms and a maximum delay of 1 second
retrier := retry.NewRetrier(5, 100 * time.Millisecond, time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()

err := retrier.RunContext(ctx, func(ctx context.Context) error {
    req, _ := http.NewRequest("GET", "http://golang.org/notfastenough", nil)
    req = req.WithContext(ctx)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("OMG AWFUL CODE %d", resp.StatusCode)
        // or decide not to retry
    }
    return nil
})
if err != nil {
    // handle error
}

Alternatives

Some of the other libs I considered:

  • jpillora/backoff
    • A little more bare bones than I wanted and no builtin concurrency safety. No context support.
  • cenkalti/backoff
    • A good library, but has some issues with context deadlines/timeouts. Can't "share" backup strategies / not thread-safe.
  • gopkg.in/retry.v1
    • Iterator-based and a little awkward for me to use personally. I preferred to abstract the loop away.
  • eapache/go-resiliency/retrier
    • Of the alternatives, I like this one the most, but I found the slice of time.Duration odd
    • No context support
    • Classifier pattern is not a bad idea, but it really comes down to "do I want to retry or stop?" and I thought it would be more flexible to simply allow the user to implement this logic how they saw fit. I could be open to changing my mind, if the solution is right. PRs welcome ;)

If you're doing HTTP work there are some good alternatives out there that add a layer on top of the standard libraries as well as providing special logic to help you automatically determine whether or not to retry a request.

  • hashicorp/go-retryablehttp
    • A very good library, but it requires conversion of all io.Readers to io.ReadSeekers, and one of my use-cases didn't allow for unconstrained cacheing of POST bodies.
  • facebookgo/httpcontrol
    • A great fully-featured transport. Only retries GETs, though :(
  • sethgrid/pester
    • Another good client, but had more options than I needed and also caches request bodies transparently.

Reference

See:

Documentation

Index

Examples

Constants

View Source
const (
	DefaultMaxTries     = 5
	DefaultInitialDelay = time.Millisecond * 200
	DefaultMaxDelay     = time.Millisecond * 1000
)

Default backoff

Variables

This section is empty.

Functions

func Stop

func Stop(err error) error

Stop signals retry that the error we are returning is a terminal error, which means we no longer wish to continue retrying the code

Types

type Retrier

type Retrier struct {
	// contains filtered or unexported fields
}

Retrier retries code blocks with or without context using an exponential backoff algorithm with jitter. It is intended to be used as a retry policy, which means it is safe to create and use concurrently.

func NewRetrier

func NewRetrier(maxTries int, initialDelay, maxDelay time.Duration) *Retrier

NewRetrier returns a retrier for retrying functions with expoential backoff. If any of the values are <= 0, they will be set to their respective defaults.

func (*Retrier) Run

func (r *Retrier) Run(funcToRetry func() error) error

Run runs a function until it returns nil, until it returns a terminal error, or until it has failed the maximum set number of iterations

Example
retrier := NewRetrier(5, 50*time.Millisecond, 50*time.Millisecond)
err := retrier.Run(func() error {
	resp, err := http.Get("http://golang.org")
	switch {
	case err != nil:
		return err
	case resp.StatusCode == 0 || resp.StatusCode >= 500:
		return fmt.Errorf("Retryable HTTP status: %s", http.StatusText(resp.StatusCode))
	case resp.StatusCode != 200:
		return Stop(fmt.Errorf("Non-retryable HTTP status: %s", http.StatusText(resp.StatusCode)))
	}
	return nil
})
fmt.Println(err)
Output:

func (*Retrier) RunContext

func (r *Retrier) RunContext(ctx context.Context, funcToRetry func(context.Context) error) error

RunContext runs a function until it returns nil, until it returns a terminal error, until its context is done, or until it has failed the maximum set number of iterations.

Note: it is the responsibility of the called function to do its part in honoring context deadlines. retry has no special magic around this, and will simply stop the retry loop when the function returns if the context is done.

Example (Output)
retrier := NewRetrier(5, 50*time.Millisecond, 50*time.Millisecond)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()

err := retrier.RunContext(ctx, func(ctx context.Context) error {
	req, _ := http.NewRequest("GET", "http://golang.org/notfastenough", nil)
	req = req.WithContext(ctx)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("OMG AWFUL CODE %d", resp.StatusCode)
		// or decide not to retry
	}
	return nil
})
fmt.Println(err)
Output:

Get "http://golang.org/notfastenough": context deadline exceeded

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL