retry

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jan 24, 2022 License: Apache-2.0 Imports: 6 Imported by: 9

Documentation

Overview

Package retry implements an efficient loop-based retry mechanism that allows the retry policy to be specified independently of the control structure. It supports exponential (with jitter) and linear retry policies.

Although syntactically lightweight, it's also flexible - for example, it can be used to run a backoff loop while waiting for other concurrent events, or with mocked-out time.

Example (Multiple)

This example demonstrates running multiple retry iterators simultaneously, using container/heap to manage them.

package main

import (
	"container/heap"
	"context"
	"fmt"
	"log"
	"time"

	"github.com/rogpeppe/retry"
)

// This example demonstrates running multiple retry iterators
// simultaneously, using container/heap to manage them.
func main() {
	log.SetFlags(log.Lmicroseconds)

	ctx := context.Background()
	var h retryHeap
	for i := 0; i < 5; i++ {
		h.Push(retryItem{
			name: fmt.Sprintf("item%d", i),
			iter: multipleStrategy.Start(),
		})
	}
	for {
		var c <-chan time.Time
		for h.Len() > 0 {
			t, ok := h[0].iter.TryTime()
			if !ok {
				log.Printf("%s finished\n", h[0].name)
				heap.Pop(&h)
				continue
			}
			if wait := time.Until(t); wait > 0 {
				c = time.After(wait)
				break
			}
			log.Printf("%s try %d\n", h[0].name, h[0].iter.Count())
			h[0].iter.NextTime()
			heap.Fix(&h, 0)
		}
		if c == nil {
			log.Printf("all done\n")
			return
		}
		select {
		case <-c:
		case <-ctx.Done():
			log.Printf("context is done: %v", ctx.Err())
			return
		}
	}
}

var multipleStrategy = retry.Strategy{
	Delay:       100 * time.Millisecond,
	MaxDelay:    5 * time.Second,
	MaxDuration: 10 * time.Second,
	Factor:      2,
	Regular:     true,
}

type retryItem struct {
	iter *retry.Iter
	name string
}

type retryHeap []retryItem

func (h retryHeap) Less(i, j int) bool {
	t1, _ := h[i].iter.TryTime()
	t2, _ := h[j].iter.TryTime()
	return t1.Before(t2)
}

func (h retryHeap) Swap(i, j int) {
	h[i], h[j] = h[j], h[i]
}

func (h retryHeap) Len() int {
	return len(h)
}

func (h *retryHeap) Push(x interface{}) {
	*h = append(*h, x.(retryItem))
}

func (h *retryHeap) Pop() interface{} {
	n := len(*h)
	i := (*h)[n-1]
	(*h) = (*h)[:n-1]
	return i
}
Output:

Example (Simple)
package main

import (
	"fmt"
	"log"
	"math/rand"
	"time"

	"github.com/rogpeppe/retry"
)

type Foo struct{}

func main() {
	log.SetFlags(log.Lmicroseconds)
	_, err := getFooWithRetry()
	if err != nil {
		log.Printf("getFooWithRetry: %v", err)
	} else {
		log.Printf("getFooWithRetry: ok")
	}
}

var retryStrategy = retry.Strategy{
	Delay:       100 * time.Millisecond,
	MaxDelay:    5 * time.Second,
	MaxDuration: 10 * time.Second,
	Factor:      2,
}

// getFooWithRetry demonstrates a retry loop.
func getFooWithRetry() (*Foo, error) {
	for i := retryStrategy.Start(); ; {
		log.Printf("getting foo")
		foo, err := getFoo()
		if err == nil {
			return foo, nil
		}
		if !i.Next(nil) {
			return nil, fmt.Errorf("error getting foo after %d tries: %v", i.Count(), err)
		}
	}
}

func getFoo() (*Foo, error) {
	if rand.Intn(5000) == 0 {
		return &Foo{}, nil
	}
	return nil, fmt.Errorf("some error")
}
Output:

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Iter

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

Iter represents a particular retry iteration loop using some strategy.

func (*Iter) Count

func (i *Iter) Count() int

Count returns the number of iterations so far. Specifically, this returns the number of times that Next or NextTime have returned true.

func (*Iter) Next

func (i *Iter) Next(stop <-chan struct{}) bool

Next sleeps until the next iteration is to be made and reports whether there are any more iterations remaining.

If a value is received on the stop channel, it immediately stops waiting for the next iteration and returns false. i.WasStopped can be called to determine if that happened.

func (*Iter) NextTime

func (i *Iter) NextTime() (time.Time, bool)

NextTime is similar to Next except that it instead returns immediately with the time that the next iteration should begin. The caller is responsible for actually waiting until that time.

func (*Iter) Reset added in v0.0.6

func (i *Iter) Reset(strategy *Strategy, now func() time.Time)

Reset is like Strategy.Start but initializes an existing Iter value which can save the allocation of the underlying time.Timer used when Next is called with a non-nil stop channel.

It also accepts a function that is used to get the current time. If that's nil, time.Now will be used.

It's OK to call this on the zero Iter value.

func (*Iter) StartTime added in v0.1.0

func (i *Iter) StartTime() time.Time

StartTime returns the time that the iterator was created or last reset.

func (*Iter) TryTime added in v0.1.0

func (i *Iter) TryTime() (time.Time, bool)

TryTime returns the time that the current try iteration should be made at, if there should be one. If iteration has finished, it returns (time.Time{}, false).

The returned time can be in the past (after Start or Reset or Next have been called) or in the future (after NextTime has been called, TryTime returns the same values that NextTime returned).

Calling TryTime repeatedly will return the same values until Next or NextTime or Reset have been called.

func (*Iter) WasStopped

func (i *Iter) WasStopped() bool

WasStopped reports whether the most recent call to Next was stopped because a value was received on its stop channel.

type Strategy

type Strategy struct {
	// Delay holds the amount of time between the start of each iteration.
	// If Factor is greater than 1 or MaxDelay is greater
	// than Delay, then the maximum delay time will increase
	// exponentially (modulo jitter) as iterations continue, up to a
	// maximum of MaxDuration if that's non-zero.
	Delay time.Duration

	// MaxDelay holds the maximum amount of time between
	// the start of each iteration. If this is greater than Delay,
	// the strategy is exponential - the time between iterations
	// will multiply by Factor on each iteration.
	MaxDelay time.Duration

	// Factor holds the exponential factor used when calculating the
	// next iteration delay. If the strategy is exponential (MaxDelay > Delay),
	// and Factor is <= 1, it will be treated as 2.
	//
	// If the initial delay is 0, it will be treated as 1ns before multiplying
	// for the first time, avoiding an infinite loop.
	//
	// The actual delay will be randomized by applying jitter
	// according to the "Full Jitter" algorithm described in
	// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
	Factor float64

	// Regular specifies that the backoff timing should be adhered
	// to as closely as possible with no jitter. When this is false,
	// the actual delay between iterations will be chosen randomly
	// from the interval (0, d] where d is the delay for that iteration.
	Regular bool

	// MaxCount limits the total number of iterations. If it's zero,
	// the count is unlimited.
	MaxCount int

	// MaxDuration limits the total amount of time taken by the
	// whole loop. An iteration will not be started if the time
	// since Start was called exceeds MaxDuration. If MaxDuration <= 0,
	// there is no time limit.
	MaxDuration time.Duration
}

Strategy represents a retry strategy. This specifies how a set of retries should be made and can be reused for any number of loops (it's treated as immutable by this package).

If an iteration takes longer than the delay for that iteration, the next iteration will be moved accordingly. For example, if the strategy has a delay of 1ms and the first two tries take 1s and 0.5ms respectively, then the second try will start immediately after the first (at 1s), but the third will start at 1.1s.

All strategies will loop for at least one iteration. The only time a loop might terminate immediately is when a value is received on the stop channel.

func ParseStrategy added in v0.0.8

func ParseStrategy(s string) (*Strategy, error)

ParseStrategy parses a string representation of a strategy. It takes the form of space-separated attribute=value pairs, where the available attributes are the lower-cased field names from the Strategy type. Values of time.Duration type are parsed with time.ParseDuration; floats are parsed with strconv.ParseFloat and boolean values must be either "true" or "false".

Fields may be in any order but must not be repeated.

For example:

delay=1ms maxdelay=1s regular=true factor=1.5 maxcount=10 maxduration=5m

The delay field is mandatory.

func (*Strategy) Start

func (s *Strategy) Start() *Iter

Start starts a retry loop using s as a retry strategy and returns an Iter that can be used to wait for each retry in turn. Note: the first try should be made immediately after calling Start without calling Next.

func (*Strategy) String added in v0.0.5

func (s *Strategy) String() string

String returns the strategy in the format understood by ParseStrategy.

Jump to

Keyboard shortcuts

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