again

package module
v1.2.2 Latest Latest
Warning

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

Go to latest
Published: Jan 22, 2026 License: MPL-2.0 Imports: 10 Imported by: 0

README

go-again

Go CodeQL

go-again thread safely wraps a given function and executes it until it returns a nil error or exceeds the maximum number of retries. The configuration consists of the maximum number of retries (after the first attempt), the interval, a jitter to add a randomized backoff, the timeout, and a registry to store errors that you consider temporary, hence worth a retry.

The Do method takes a context, a function, and an optional list of temporary errors as arguments. If the list is omitted and the registry has entries, the registry is used as the default filter; if the registry is empty, all errors are retried. It supports cancellation from the context and a channel invoking the Cancel() function. For long-running operations, use DoWithContext and observe cancellation inside the retryable function. The returned type is Errors which contains the list of errors returned at each attempt and the last error returned by the function.

// Errors holds the error returned by the retry function along with the trace of each attempt.
type Errors struct {
    // Attempts holds the trace of each attempt in order.
    Attempts []error
    // Last holds the last error returned by the retry function.
    Last error
}

When you pass a list of temporary errors to Do, retries only happen when the error matches that list. The registry is a convenience store for temporary errors you want to pass to Do, or to use as the default filter when the list is omitted:

    // Init with defaults.
    retrier, err := again.NewRetrier(context.Background())
    if err != nil {
        // handle error
    }

    retrier.Registry.RegisterTemporaryError(http.ErrAbortHandler)

    defer retrier.Registry.UnRegisterTemporaryError(http.ErrAbortHandler)

    var retryCount int

    errs := retrier.Do(context.TODO(), func() error {
        retryCount++
        if retryCount < 3 {
            return http.ErrAbortHandler
        }

        return nil
    }, http.ErrAbortHandler)

    if errs.Last != nil {
        // handle error
    }

Should you retry regardless of the error returned, call Do without passing any temporary errors and keep the registry empty:

    var retryCount int

    retrier, err := again.NewRetrier(context.Background(), again.WithTimeout(1*time.Second),
        again.WithJitter(500*time.Millisecond),
        again.WithMaxRetries(3))

    if err != nil {
        // handle error
    }

    errs := retrier.Do(context.TODO(), func() error {
        retryCount++
        if retryCount < 3 {
            return http.ErrAbortHandler
        }

        return nil
    })
    if errs.Last != nil {
        // handle error
    }

For long-running operations, pass a context-aware function:

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    errs := retrier.DoWithContext(ctx, func(ctx context.Context) error {
        // Do work and return ctx.Err() if canceled.
        return nil
    })
    if errs.Last != nil {
        // handle error
    }

It's also possible to create a Registry with the temporary default errors: retrier.Registry.LoadDefaults(). You can extend the list with your errors by calling the RegisterTemporaryError method.

Walk through the documentation for further details about the settings, the programmability, the implementation.

Performance

A retrier certainly adds overhead to the execution of a function. go-again is designed to produce a minimal impact on the performance of your code, keeping thread safety and flexibility. The following benchmark shows the overhead of a retrier with 5 retries, 1s interval, 10ms jitter, and 1s timeout:

go test -bench=. -benchtime=3s -benchmem -run=^-memprofile=mem.out ./...
?    github.com/hyp3rd/go-again [no test files]
goos: darwin
goarch: arm64
pkg: github.com/hyp3rd/go-again/tests
cpu: Apple M2 Pro
BenchmarkRetry-12                12622006       277.6 ns/op       48 B/op        1 allocs/op
BenchmarkRetryWithRetries-12        93937       38345 ns/op      144 B/op        2 allocs/op
PASS
ok   github.com/hyp3rd/go-again/tests 8.498s

Installation

go get github.com/hyp3rd/go-again

Usage

For examples with cancellation, see examples. To run the examples you can leverage the Makefile:

make run example=chan
make run example=context
Retrier
package main

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

    "github.com/hyp3rd/ewrap"

    "github.com/hyp3rd/go-again"
)

func main() {
    // Create a new retrier.
    retrier, err := again.NewRetrier(context.Background(), again.WithTimeout(1*time.Second),
        again.WithJitter(500*time.Millisecond),
        again.WithMaxRetries(3))

    if err != nil {
        // handle error
    }

    // Register a temporary error.
    tempErr := ewrap.New("temporary error")
    retrier.Registry.RegisterTemporaryError(tempErr)

    // Retry a function.
    errs := retrier.Do(context.TODO(), func() error {
        // Do something here.
        return tempErr
    }, tempErr)
    if errs.Last != nil {
        // handle error
    }
}
Scheduler

The scheduler runs HTTP requests on an interval and posts results to a callback URL. Request and callback URLs are validated with sectools (HTTPS only, no userinfo, and no private/localhost hosts by default). To customize or disable validation, pass WithURLValidator (use nil to disable validation).

package main

import (
    "context"
    "net/http"
    "time"

    "github.com/hyp3rd/go-again"
    "github.com/hyp3rd/go-again/pkg/scheduler"
)

func main() {
    retrier, _ := again.NewRetrier(
        context.Background(),
        again.WithMaxRetries(5),
        again.WithInterval(10*time.Millisecond),
        again.WithJitter(10*time.Millisecond),
        again.WithTimeout(5*time.Second),
    )

    s := scheduler.NewScheduler()
    defer s.Stop()

    _, _ = s.Schedule(scheduler.Job{
        Schedule: scheduler.Schedule{
            Every:   1 * time.Minute,
            MaxRuns: 1,
        },
        Request: scheduler.Request{
            Method: http.MethodPost,
            URL:    "https://example.com/endpoint",
        },
        Callback: scheduler.Callback{
            URL: "https://example.com/callback",
        },
        RetryPolicy: scheduler.RetryPolicy{
            Retrier:          retrier,
            RetryStatusCodes: []int{http.StatusTooManyRequests, http.StatusInternalServerError},
        },
    })
}

Supported methods for request and callback: GET, POST, PUT. If the callback URL is empty, no callback is sent.

To relax URL validation for local HTTPS targets (localhost/private IPs):

import (
    "github.com/hyp3rd/go-again/pkg/scheduler"
    "github.com/hyp3rd/sectools/pkg/validate"
)

validator, _ := validate.NewURLValidator(
    validate.WithURLAllowPrivateIP(true),
    validate.WithURLAllowLocalhost(true),
    validate.WithURLAllowIPLiteral(true),
)

s := scheduler.NewScheduler(
    scheduler.WithURLValidator(validator),
)

To allow non-HTTPS endpoints, disable validation entirely:

s := scheduler.NewScheduler(
    scheduler.WithURLValidator(nil),
)
Scheduler Options
  • WithHTTPClient uses a custom HTTP client for requests and callbacks.
  • WithLogger sets the slog.Logger used for scheduler warnings.
  • WithConcurrency limits concurrent executions.
  • WithURLValidator sets the URL validator (pass nil to disable).
Schedule Fields
  • Every is required.
  • StartAt and EndAt are optional time bounds.
  • MaxRuns caps the number of scheduled executions when > 0.
Retry Policy
  • RetryStatusCodes marks response codes as retryable.
  • TemporaryErrors adds retryable error types.
  • If Retrier is nil, a default retrier is created and registry defaults are loaded.
Callback Payload
  • Fields: job_id, scheduled_at, started_at, finished_at, attempts, success, status_code, error, response_body.
  • response_body is limited by Callback.MaxBodyBytes (defaults to 4096).

License

The code and documentation in this project are released under Mozilla Public License 2.0.

Author

I'm a surfer, a crypto trader, and a software architect with 15 years of experience designing highly available distributed production environments and developing cloud-native apps in public and private clouds. Feel free to hook me up on LinkedIn.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidRetrier is the error returned when the retrier is invalid.
	ErrInvalidRetrier = ewrap.New("invalid retrier")
	// ErrMaxRetriesReached is the error returned when the maximum number of retries is reached.
	ErrMaxRetriesReached = ewrap.New("maximum number of retries reached")
	// ErrTimeoutReached is the error returned when the timeout is reached.
	ErrTimeoutReached = ewrap.New("operation timeout reached")
	// ErrOperationStopped is the error returned when the retry is stopped.
	ErrOperationStopped = ewrap.New("operation stopped")
	// ErrNilRetryableFunc is the error returned when the retryable function is nil.
	ErrNilRetryableFunc = ewrap.New("failed to invoke the function. It appears to be nil")
)
View Source
var ErrOperationFailed = ewrap.New("failed")

ErrOperationFailed is the error returned when the operation fails.

Functions

This section is empty.

Types

type Errors added in v1.0.7

type Errors struct {
	// Attempts holds the trace of each attempt in order.
	Attempts []error
	// Last holds the last error returned by the retry function.
	Last error
}

Errors holds the error returned by the retry function along with the trace of each attempt.

func DoWithResult added in v1.1.2

func DoWithResult[T any](ctx context.Context, r *Retrier, fn func() (T, error), temporaryErrors ...error) (T, *Errors)

DoWithResult retries a function that returns a result and an error.

func (*Errors) Join added in v1.1.2

func (e *Errors) Join() error

Join aggregates all attempt errors into one.

func (*Errors) Reset added in v1.1.2

func (e *Errors) Reset()

Reset clears stored errors.

type Hooks added in v1.1.2

type Hooks struct {
	// OnRetry is called before waiting for the next retry interval.
	OnRetry func(attempt int, err error)
}

Hooks defines callback functions invoked by the retrier.

type Option added in v1.0.5

type Option func(*Retrier)

Option is a function type that can be used to configure the `Retrier` struct.

func WithBackoffFactor added in v1.0.9

func WithBackoffFactor(factor float64) Option

WithBackoffFactor returns an option that sets the backoff factor.

func WithHooks added in v1.1.2

func WithHooks(h Hooks) Option

WithHooks sets hooks executed during retries.

func WithInterval added in v1.0.5

func WithInterval(interval time.Duration) Option

WithInterval returns an option that sets the interval.

func WithJitter added in v1.0.5

func WithJitter(jitter time.Duration) Option

WithJitter returns an option that sets the jitter.

func WithLogger added in v1.1.2

func WithLogger(logger *slog.Logger) Option

WithLogger sets a slog logger.

func WithMaxRetries added in v1.0.5

func WithMaxRetries(num int) Option

WithMaxRetries returns an option that sets the maximum number of retries after the first attempt.

func WithTimeout added in v1.0.5

func WithTimeout(timeout time.Duration) Option

WithTimeout returns an option that sets the timeout.

type Registry added in v1.0.9

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

Registry holds a set of temporary errors.

func NewRegistry added in v1.0.1

func NewRegistry() *Registry

NewRegistry creates a new Registry.

func (*Registry) Clean added in v1.0.9

func (r *Registry) Clean()

Clean removes all temporary errors from the registry.

func (*Registry) IsTemporaryError added in v1.1.0

func (r *Registry) IsTemporaryError(err error, errs ...error) bool

IsTemporaryError reports whether err matches any of the temporary errors.

func (*Registry) Len added in v1.1.1

func (r *Registry) Len() int

Len returns the number of registered temporary errors.

func (*Registry) ListTemporaryErrors added in v1.0.9

func (r *Registry) ListTemporaryErrors() []error

ListTemporaryErrors returns all temporary errors in the registry.

func (*Registry) LoadDefaults added in v1.0.9

func (r *Registry) LoadDefaults() *Registry

LoadDefaults loads a set of default temporary errors.

func (*Registry) RegisterTemporaryError added in v1.0.9

func (r *Registry) RegisterTemporaryError(err error)

RegisterTemporaryError registers a temporary error.

func (*Registry) RegisterTemporaryErrors added in v1.0.9

func (r *Registry) RegisterTemporaryErrors(errs ...error)

RegisterTemporaryErrors registers multiple temporary errors.

func (*Registry) UnRegisterTemporaryError added in v1.0.9

func (r *Registry) UnRegisterTemporaryError(err error)

UnRegisterTemporaryError removes a temporary error.

func (*Registry) UnRegisterTemporaryErrors added in v1.0.9

func (r *Registry) UnRegisterTemporaryErrors(errs ...error)

UnRegisterTemporaryErrors removes multiple temporary errors.

type Retrier

type Retrier struct {
	// MaxRetries is the maximum number of retries after the first attempt.
	MaxRetries int
	// Jitter is the amount of jitter to apply to the retry interval.
	Jitter time.Duration
	// BackoffFactor is the factor to apply to the retry interval.
	BackoffFactor float64
	// Interval is the interval between retries.
	Interval time.Duration
	// Timeout is the timeout for the retry function.
	Timeout time.Duration
	// Registry is the registry for temporary errors.
	Registry *Registry

	// Logger used for logging attempts.
	Logger *slog.Logger
	// Hooks executed during retries.
	Hooks Hooks
	// contains filtered or unexported fields
}

Retrier is a type that retries a function until it returns a nil error or the maximum number of retries is reached.

func NewRetrier

func NewRetrier(ctx context.Context, opts ...Option) (retrier *Retrier, err error)

NewRetrier returns a new Retrier configured with the given options. If no options are provided, the default options are used. The default options are:

  • MaxRetries: 5 (retries after the first attempt)
  • Jitter: 1 * time.Second
  • Interval: 500 * time.Millisecond
  • Timeout: 20 * time.Second

func (*Retrier) Cancel added in v1.0.4

func (r *Retrier) Cancel()

Cancel cancels the retries notifying the `Do` function to return.

func (*Retrier) Do added in v1.0.7

func (r *Retrier) Do(ctx context.Context, retryableFunc RetryableFunc, temporaryErrors ...error) (errs *Errors)

Do retries a `retryableFunc` until it returns a nil error or the maximum number of retries is reached.

  • If the maximum number of retries is reached, the function returns an `Errors` object.
  • If the `retryableFunc` returns a nil error, the function assigns an `Errors.Last` before returning.
  • If the `retryableFunc` returns a temporary error, the function retries the function.
  • If the `retryableFunc` returns a non-temporary error, the function assigns the error to `Errors.Last` and returns.
  • If the `temporaryErrors` list is empty and the registry has entries, only those errors are retried.
  • If the `temporaryErrors` list is empty and the registry is empty, all errors are retried.
  • The context is checked between attempts; long-running functions should handle cancellation themselves.

func (*Retrier) DoWithContext added in v1.2.0

func (r *Retrier) DoWithContext(ctx context.Context, retryableFunc RetryableFuncWithContext, temporaryErrors ...error) (errs *Errors)

DoWithContext retries a context-aware function until it succeeds, a terminal error is encountered, the provided context is canceled, the retrier timeout elapses, or the maximum number of retries is reached. It is the context-aware counterpart of Do.

The behavior of DoWithContext mirrors Do:

  • The retryableFunc is invoked at most MaxRetries+1 times (the initial attempt plus up to MaxRetries retries) until it returns a nil error.
  • Any error returned by retryableFunc that matches one of the temporaryErrors is treated as transient. That error is appended to errs.Attempts and the call is retried according to the configured backoff, jitter, and interval settings.
  • Any error that does not match temporaryErrors is considered terminal. In that case, the retry loop stops immediately and errs.Last is set to that error without performing further retries.
  • If the Retrier is configured with a registry, attempt and error information are recorded in the registry in the same way as for Do.

When the maximum number of retries (MaxRetries) is reached without a successful (nil) result, the last attempt error is wrapped with ErrMaxRetriesReached and appended to errs.Attempts. In this case, errs.Last contains the last error returned by retryableFunc.

Context and timeout handling:

  • The provided ctx is observed on each attempt and during backoff delays. If ctx is canceled or its deadline is exceeded, DoWithContext stops retrying and returns immediately, with errs.Last set to the corresponding context error or any wrapped form produced by the internals of the retrier.
  • In addition to ctx, the Retrier's Timeout field enforces an overall timeout for the entire operation. If this timeout elapses first, DoWithContext stops retrying and returns with errs.Last set to ErrTimeoutReached (wrapped with the attempt number).

Use DoWithContext when the operation being retried accepts a context and must support cancellation. Use Do for retrying functions that do not take a context.

func (*Retrier) PutErrors added in v1.1.2

func (r *Retrier) PutErrors(errs *Errors)

PutErrors returns an Errors object to the pool after resetting it.

func (*Retrier) SetRegistry

func (r *Retrier) SetRegistry(reg *Registry) error

SetRegistry sets the registry for temporary errors. Use this function to set a custom registry if: - you want to add custom temporary errors. - you want to remove the default temporary errors. - you want to replace the default temporary errors with your own. - you have initialized the Retrier without using the constructor `NewRetrier`.

func (*Retrier) Stop added in v1.1.2

func (r *Retrier) Stop()

Stop stops the retries.

func (*Retrier) Validate added in v1.0.8

func (r *Retrier) Validate() error

Validate validates the Retrier. This method will check if:

  • `MaxRetries` is less than zero
  • `Interval` is greater than or equal to `Timeout`
  • The total time consumed by all retries (`Interval` multiplied by `MaxRetries`) should be less than `Timeout`.

type RetryableFunc added in v1.0.5

type RetryableFunc func() error

RetryableFunc signature of retryable function.

type RetryableFuncWithContext added in v1.2.0

type RetryableFuncWithContext func(ctx context.Context) error

RetryableFuncWithContext is a retryable function that observes context cancellation.

type TimerPool added in v1.0.9

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

TimerPool is a pool of timers.

func NewTimerPool added in v1.0.4

func NewTimerPool(size int, timeout time.Duration) *TimerPool

NewTimerPool creates a new timer pool.

func (*TimerPool) Close added in v1.0.9

func (p *TimerPool) Close()

Close closes the pool.

func (*TimerPool) Drain added in v1.0.9

func (p *TimerPool) Drain()

Drain drains the pool.

func (*TimerPool) Get added in v1.0.9

func (p *TimerPool) Get() *time.Timer

Get retrieves a timer from the pool.

func (*TimerPool) Len added in v1.0.9

func (p *TimerPool) Len() int

Len returns the number of timers in the pool.

func (*TimerPool) Put added in v1.0.9

func (p *TimerPool) Put(timer *time.Timer)

Put returns a timer back into the pool.

Directories

Path Synopsis
__examples
chan command
context command
timeout command
validate command
pkg

Jump to

Keyboard shortcuts

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