retry

package
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 7, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package retry re-attempts work that failed for a reason that might not recur.

The whole subject is one judgement call: retrying the wrong thing is worse than not retrying at all. A retried POST that actually succeeded creates a second resource. A retry loop with no jitter turns a brief dependency blip into a synchronised stampede that keeps it down. A retry of a 400 is three requests that were always going to fail.

So this package defaults to NOT retrying, and asks you to say what is retryable. Retryable() covers the usual answer: timeouts, unavailability, and serialization conflicts — never a client error, never an unclassified one.

Example

An unavailable dependency is worth another attempt.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/IncepTools/go/src/core/errs"
	"github.com/IncepTools/go/src/reliability/retry"
)

func fast() retry.Policy {
	return retry.Policy{
		MaxAttempts: 3,
		BaseDelay:   time.Millisecond,
		MaxDelay:    2 * time.Millisecond,
		NoJitter:    true,
	}
}

func main() {
	attempts := 0
	quote, err := retry.Value(context.Background(), fast(),
		func(context.Context) (string, error) {
			attempts++
			if attempts < 3 {
				return "", errs.Unavailable("the pricing service is down")
			}
			return "£12.00", nil
		})

	fmt.Println(quote, err, attempts)
}
Output:
£12.00 <nil> 3

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Do

func Do(ctx context.Context, p Policy, fn func(ctx context.Context) error) error

Do runs fn until it succeeds, the policy is exhausted, or the error is not retryable. It returns the LAST error.

The context is honoured between attempts: a cancelled caller stops waiting immediately rather than sleeping out the backoff.

Example (DoesNotRetryAClientError)

A client error is not retried. Three requests that were always going to fail help nobody.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/IncepTools/go/src/core/errs"
	"github.com/IncepTools/go/src/reliability/retry"
)

func fast() retry.Policy {
	return retry.Policy{
		MaxAttempts: 3,
		BaseDelay:   time.Millisecond,
		MaxDelay:    2 * time.Millisecond,
		NoJitter:    true,
	}
}

func main() {
	attempts := 0
	_ = retry.Do(context.Background(), fast(), func(context.Context) error {
		attempts++
		return errs.Invalid("price must be 0 or more")
	})

	fmt.Println("attempts:", attempts)
}
Output:
attempts: 1

func Retryable

func Retryable(err error) bool

Retryable reports whether an error is worth retrying: a timeout, an unavailable dependency, or a serialization conflict.

Everything else is not. In particular an unclassified error is NOT retryable — if nobody has decided what it means, it must not be assumed safe to repeat.

Example

Neither is an error nobody classified: if nothing has decided what it means, it must not be assumed safe to repeat.

package main

import (
	"errors"
	"fmt"

	"github.com/IncepTools/go/src/core/errs"
	"github.com/IncepTools/go/src/reliability/retry"
)

func main() {
	fmt.Println(retry.Retryable(errs.Unavailable("down")))
	fmt.Println(retry.Retryable(errs.Timeout("slow")))
	fmt.Println(retry.Retryable(errs.Conflict("raced").WithCode("CONCURRENT_MODIFICATION")))
	fmt.Println(retry.Retryable(errs.Conflict("taken").WithCode("SKU_TAKEN")))
	fmt.Println(retry.Retryable(errors.New("something happened")))
}
Output:
true
true
true
false
false

func Value

func Value[T any](ctx context.Context, p Policy, fn func(ctx context.Context) (T, error)) (T, error)

Value is Do for a function that returns a result.

Types

type Policy

type Policy struct {
	// MaxAttempts counts the FIRST try. 3 means one try and two retries.
	// Zero or one disables retrying.
	MaxAttempts int
	// BaseDelay is the first wait. Each subsequent wait doubles it.
	BaseDelay time.Duration
	// MaxDelay caps the exponential growth.
	MaxDelay time.Duration
	// Jitter randomises each wait by up to ±50%.
	//
	// Leave it ON. Without jitter, every client that failed at the same
	// moment retries at the same moment, and the dependency that was briefly
	// slow is now reliably overloaded.
	NoJitter bool
	// Retryable decides whether an error is worth another attempt. Defaults
	// to Retryable.
	Retryable func(error) bool
	// OnRetry is called before each wait, for logging or a metric.
	OnRetry func(attempt int, err error, wait time.Duration)
}

Policy describes how to retry.

func Default

func Default() Policy

Default is a sensible policy for a network call: three attempts, 100ms growing to 2s, jittered.

Jump to

Keyboard shortcuts

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