retry

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2022 License: Apache-2.0 Imports: 4 Imported by: 0

README

deep-rent/retry

Logo

This library provides a retry mechanism for Go based on highly configurable backoff strategies. Unlike similar libraries, the retry mechanism is initiated through a reusable struct type, which allows for easy mocking and sharing of backoff configuration.

Test Status Documentation Code Quality

Installation

Download the libary using go get:

go get github.com/deep-rent/retry@latest

Add the following imports to your project:

import (
    "github.com/deep-rent/retry"
    "github.com/deep-rent/retry/backoff"
)

Usage

First, define a function to be retried. The signature must match retry.AttemptFunc.

attempt := func(n int) error {
    if n == 5 {
        // succeed after 5 attempts
        return nil
    } else {
        // force retry
        return errors.New("whoops")
    }
}

Next, configure a retry.Cycler. Once configured, the cycler can be reused across your project.

// exponentially increase delays between consecutive attempts
cycler := retry.NewCycler(backoff.Exponential(5 * time.Second, 1.5))
cycler.Cap(3 * time.Minute)      // cap backoff to 3 minutes
cycler.Limit(25)                 // stop after 25 attempts
cycler.Timeout(10 * time.Minute) // time out after 10 minutes
cycler.Jitter(0.5)               // introduce 50% random jitter

Register a retry.ErrorHandlerFunc to catch intermediate errors.

cycler.OnError(func(n int, delay time.Duration, err error) {
    s := delay.Seconds()
    fmt.Printf("attempt #%d failed: %v => wait %4.f s\n", n, err, s)
})

Finally, retry attempt according to the previous configuration.

err := cycler.Try(attempt)
if err != nil {
    fmt.Printf("failed after retries: %v", err)
}

Another way to exit a retry cycle early is to flag the returned error using ForceExit.

attempt := func(int) error {
    // exit the retry cycle immediately
    return retry.ForceExit(errors.New("unrecoverable")) 
}

License

Licensed under the Apache 2.0 License. For the full copyright and licensing information, please view the LICENSE file that was distributed with this source code.

Documentation

Overview

Package retry implements a retry mechanism based on configurable backoff strategies.

The fundamental structure is called Cycler. A cycler can be obtained by passing an appropriate backoff.Strategy to NewCycler. Any function whose signature matches AttemptFunc can then be retried using either Cycler.Try or Cycler.TryWithContext.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ForceExit added in v1.1.0

func ForceExit(err error) error

ForceExit wraps err in an ExitError.

Types

type AttemptFunc added in v1.1.0

type AttemptFunc func(n int) error

An AttemptFunc can be scheduled in a retry cycle. The function will be retried if it returns an error, while returning nil indicates successful completion. The argument n is the current attempt count, starting at n = 1.

type Cycler

type Cycler struct {
	Clock backoff.Clock // used to track the execution time of retry cycles
	// contains filtered or unexported fields
}

A Cycler is used to schedule retry cycles in which an AttemptFunc is repeatedly executed until it succeeds. Once configured, the same cycler can be used to schedule any number of retry cycles.

Example

This example uses exponential backoff to retry a dummy function.

package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/deep-rent/retry"
	"github.com/deep-rent/retry/backoff"
)

func main() {
	exp := backoff.Exponential(2*time.Millisecond, 2.0)

	cycler := retry.NewCycler(exp)
	cycler.Cap(2 * time.Second)      // cap backoff delay at 2 seconds
	cycler.Timeout(15 * time.Second) // stop retrying after 15 seconds
	// cycler.Jitter(0.5)            // introduce 50% jitter

	// register an error handler
	cycler.OnError(func(n int, delay time.Duration, err error) {
		ms := delay.Milliseconds()
		fmt.Printf("attempt #%d: %v => wait %2d ms\n", n, err, ms)
	})

	const N = 5 // number of tries

	// start retry cycle
	err := cycler.Try(func(n int) error {
		if n == N {
			// succeed after 5 attempts
			return nil
		} else {
			// force retry
			return errors.New("failed")
		}
	})

	if err != nil {
		fmt.Printf("failed after retries: %v", err)
	} else {
		fmt.Printf("attempt #%d: succeeded", N)
	}

}
Output:

attempt #1: failed => wait  2 ms
attempt #2: failed => wait  4 ms
attempt #3: failed => wait  8 ms
attempt #4: failed => wait 16 ms
attempt #5: succeeded

func NewCycler

func NewCycler(strategy backoff.Strategy) *Cycler

NewCycler creates a new retry Cycler. The specified backoff.Strategy determines the backoff delay between consecutive attempts. A cycler is meant to be reused; recreating the same cycler should be avoided.

func (*Cycler) Cap

func (c *Cycler) Cap(max time.Duration)

Cap sets the maximum delay between consecutive attempts. If max <= 0, no limit will be applied.

func (*Cycler) Jitter

func (c *Cycler) Jitter(spread float64)

Jitter randomly spreads delays between consecutive attempts around in time. The spread factor determines the relative range in which delays are scattered. It must fall in the half-open interval [0,1). For example, a spread of 0.5 results in delays ranging between 50% above and 50% below the values produced by the underlying backoff strategy. If spread = 0, no jitter will be applied.

func (*Cycler) Limit

func (c *Cycler) Limit(n int)

Limit sets the maximum number of attempts in a retry cycle. A retry cycle will stop after the n-th attempt. If n < 1, no limit will be applied.

func (*Cycler) OnError

func (c *Cycler) OnError(handler ErrorHandlerFunc)

OnError registers a callback to be invoked when a failed AttemptFunc needs to be retried. Typically, these callbacks are used to log intermediate errors that would otherwise remain unhandled.

func (*Cycler) Timeout

func (c *Cycler) Timeout(limit time.Duration)

Timeout sets the maximum duration of retry cycles. A retry cycle will stop after the time elapsed since it was scheduled goes past the maximum. If limit <= 0, no timeout will be applied.

func (*Cycler) Try

func (c *Cycler) Try(attempt AttemptFunc) error

Try calls [TryWithContext] using context.Background.

Example

This example uses linear backoff to retry a dummy function.

package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/deep-rent/retry"
	"github.com/deep-rent/retry/backoff"
)

func main() {
	lin := backoff.Linear(5*time.Millisecond, 5*time.Millisecond)

	cycler := retry.NewCycler(lin)
	cycler.Limit(10) // stop retrying after 10 attempts

	// register an error handler
	cycler.OnError(func(n int, delay time.Duration, err error) {
		ms := delay.Milliseconds()
		fmt.Printf("attempt #%d: %v => wait %2d ms\n", n, err, ms)
	})

	const N = 5 // number of tries

	// start retry cycle
	err := cycler.Try(func(n int) error {
		if n == N {
			// succeed after 5 attempts
			return nil
		} else {
			// force retry
			return errors.New("failed")
		}
	})

	if err != nil {
		fmt.Printf("failed after retries: %v", err)
	} else {
		fmt.Printf("attempt #%d: succeeded", N)
	}

}
Output:

attempt #1: failed => wait  5 ms
attempt #2: failed => wait 10 ms
attempt #3: failed => wait 15 ms
attempt #4: failed => wait 20 ms
attempt #5: succeeded

func (*Cycler) TryWithContext

func (c *Cycler) TryWithContext(
	ctx context.Context,
	attempt AttemptFunc,
) error

TryWithContext schedules a retry cycle in which attempt is repeatedly executed until it returns nil. The cycle stops early if

  1. some limit is exceeded,
  2. ctx is cancelled, or
  3. an ExitError occurs.

When an invocation of attempt returns nil before the cycle stops, this method also returns nil. Otherwise, this method returns the last error returned by attempt. If ctx contains an error, this error will be returned instead.

In any case, attempt is guaranteed to be executed at least once. Be aware that retry cycles with neither Cycler.Limit nor Cycler.Timeout set will run forever if attempt keeps failing.

Example

This example uses a cancellable context to stop a retry cycle.

package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/deep-rent/retry"
	"github.com/deep-rent/retry/backoff"
)

func main() {
	con := backoff.Constant(10 * time.Millisecond)

	cycler := retry.NewCycler(con)
	ctx, cancel := context.WithCancel(context.Background())

	// register an error handler
	cycler.OnError(func(n int, delay time.Duration, err error) {
		ms := delay.Milliseconds()
		fmt.Printf("attempt #%d: %v => wait %2d ms\n", n, err, ms)
	})

	const N = 5 // number of tries

	// start retry cycle
	err := cycler.TryWithContext(ctx, func(n int) error {
		if n == N {
			cancel()
			// succeed after 5 attempts
			return nil
		}
		// force retry
		return errors.New("failed")
	})

	if err != nil && !errors.Is(err, context.Canceled) {
		fmt.Printf("failed after retries: %v", err)
	} else {
		fmt.Printf("attempt #%d: succeeded", N)
	}

}
Output:

attempt #1: failed => wait 10 ms
attempt #2: failed => wait 10 ms
attempt #3: failed => wait 10 ms
attempt #4: failed => wait 10 ms
attempt #5: succeeded

type ErrorHandlerFunc added in v1.1.0

type ErrorHandlerFunc func(n int, delay time.Duration, err error)

An ErrorHandlerFunc is invoked when the n-th execution of an AttemptFunc failed with err, and the next retry is pending after delay has passed. Note that the initial execution corresponds to n = 1.

type ExitError added in v1.1.0

type ExitError struct {
	Cause error
}

An ExitError signals that an AttemptFunc should no longer be retried. Use ForceExit to wrap an error such that it forces the current retry cycle to exit. This is useful when an error is encountered that the program cannot possibly recover after additional retries.

func (*ExitError) Error added in v1.1.0

func (e *ExitError) Error() string

Directories

Path Synopsis
Package backoff provides various backoff strategies.
Package backoff provides various backoff strategies.

Jump to

Keyboard shortcuts

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