retry

package
v3.0.0-next.11 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 8 Imported by: 0

README

Retry Package

Go Reference

Production-ready retry mechanism with exponential backoff using cenkalti/backoff/v4. This package provides a clean, reusable API for retrying operations without manual retry logic implementation.

Features

  • Exponential Backoff: Configurable backoff strategy with jitter
  • Context Support: Respects context cancellation and timeouts
  • OpenTelemetry Integration: Automatic tracing and logging
  • Permanent Errors: Stop retrying for non-transient errors
  • Functional Options: Sensible defaults via DefaultConfig, overridden with retry.New(...) options
  • No Panics: Invalid configuration is reported as an error by Do/DoWithNotify before the first attempt

Installation

go get github.com/jasoet/pkg/v3/retry

Quick Start

Configure with functional options

retry.New starts from DefaultConfig() and applies each option in order; fields you don't set keep their defaults.

cfg := retry.New(
    retry.WithName("db.connect"),
    retry.WithMaxRetries(3),
    retry.WithInitialInterval(100*time.Millisecond),
)

Backed by ExampleNew.

Retry an operation

Do calls the operation, and retries it with exponential backoff while it returns an error. With MaxRetries = N the operation runs at most N+1 times (1 initial attempt + up to N retries). Here the operation fails twice and succeeds on the third attempt:

cfg := retry.New(
    retry.WithName("flaky.op"),
    retry.WithMaxRetries(5),
    retry.WithInitialInterval(time.Millisecond),
)

attempts := 0
err := retry.Do(ctx, cfg, func(ctx context.Context) error {
    attempts++
    if attempts < 3 {
        return errors.New("temporary failure")
    }
    return nil
})
// attempts == 3, err == nil

Backed by ExampleDo.

Permanent errors (no retry)

Wrap an error with retry.Permanent to stop retrying immediately — useful for validation errors, 4xx HTTP responses, and other non-transient failures. Do returns after the first attempt; the returned error wraps the original one, so errors.Is/errors.As still match it.

err := retry.Do(ctx, cfg, func(ctx context.Context) error {
    return retry.Permanent(errors.New("invalid input"))
})
// 1 attempt; err == "validate.input failed after 1 attempts (1 initial + 0 retries): invalid input"

Backed by ExamplePermanent.

Custom notifications

DoWithNotify calls a notify function before each retry wait, with the error and the upcoming backoff duration — handy for custom logging or metrics:

err := retry.DoWithNotify(ctx, cfg,
    func(ctx context.Context) error {
        return riskyOperation()
    },
    func(err error, backoff time.Duration) {
        log.Printf("retrying in %v: %v", backoff, err)
    },
)

Backed by ExampleDoWithNotify.

Unlimited retries (use with a timeout)

retry.WithMaxRetries(0) means unlimited retries — the loop ends only when the operation succeeds or the context is done. Always combine it with context.WithTimeout (or a deadline) so the loop terminates.

Configuration

Config fields

All fields are exported and carry yaml/mapstructure tags (camelCase), so a Config can also be loaded from a config file.

Field Type Default Notes
MaxRetries uint64 5 Retries after the initial attempt; 0 means unlimited
InitialInterval time.Duration 500ms Wait before the first retry; must be > 0
MaxInterval time.Duration 60s Cap for the backoff interval; must be >= InitialInterval
Multiplier float64 2.0 Backoff growth per retry; must be > 1
RandomizationFactor float64 0.5 Jitter factor in [0, 1]; 0 disables jitter, 0.5 means +/-50%
Name string "retry.operation" Operation name used in log messages, error messages, and OTel spans
OTelConfig *otel.Config nil OpenTelemetry instrumentation; nil disables it. Not serializable (yaml:"-")
Options
  • WithName(name string) — operation name for logging/tracing
  • WithMaxRetries(n uint64) — max retries after the initial attempt (0 = unlimited)
  • WithInitialInterval(d time.Duration) — initial retry interval
  • WithMaxInterval(d time.Duration) — retry interval cap
  • WithMultiplier(m float64) — exponential backoff multiplier
  • WithRandomizationFactor(f float64) — jitter factor in [0, 1]
  • WithOTelConfig(cfg *otel.Config) — OpenTelemetry instrumentation
Validation (no panics)

Options never panic. Do and DoWithNotify validate the config before the first attempt and return a descriptive error if any rule is violated — the operation is never called with an invalid config:

  • Multiplier must be > 1
  • InitialInterval must be > 0
  • MaxInterval must be >= InitialInterval
  • RandomizationFactor must be in [0, 1]

How It Works

  1. Exponential backoff: each retry waits InitialInterval × Multiplier^(retry-1), capped at MaxInterval. There is no overall time limit (MaxElapsedTime is disabled); termination is governed by MaxRetries and context.
  2. Jitter: RandomizationFactor spreads intervals to prevent a thundering herd.
  3. Context awareness: cancellation and deadlines stop the retry loop immediately; the returned error wraps ctx.Err().
  4. Permanent errors: retry.Permanent(err) short-circuits the retry loop on the current attempt.

Examples

See examples/retry for a runnable program covering basic retry, custom backoff, permanent errors, context cancellation, unlimited retries with timeout, and custom notifications.

Best Practices

  1. Set appropriate MaxRetries: don't retry forever; use a context timeout for long-running polls.
  2. Use permanent errors: mark non-transient errors with retry.Permanent to avoid pointless retries.
  3. Size the backoff for the dependency: fast in-process operations ~100ms initial / 1.5x multiplier; network calls 500ms–1s / 2x; heavy operations 1s+ / 2–3x.
  4. Keep jitter on in production (RandomizationFactor > 0) so synchronized clients don't stampede a recovering service.
  5. Name your operations: WithName shows up in error messages and OTel spans — invaluable when several retried operations interleave.
  6. Monitor with OTel: pass an *otel.Config via WithOTelConfig for tracing and structured logs.

Testing

go test -v -race ./retry

The suite covers success/failure paths, backoff timing, context cancellation, permanent errors, unlimited retries, notifications, and Do-time config validation, plus compile-checked example tests in example_test.go.

  • cenkalti/backoff — underlying backoff implementation
  • otel — OpenTelemetry integration for observability

License

MIT License - see LICENSE for details

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Do

func Do(ctx context.Context, cfg Config, operation Operation) error

Do executes the operation with retry logic using exponential backoff. It returns nil if the operation succeeds, or the last error if all retries are exhausted. An invalid Config (see Config field docs) is reported as an error before the first attempt; Do never panics.

Example:

cfg := retry.New(
	retry.WithName("database.connect"),
	retry.WithMaxRetries(3),
	retry.WithOTelConfig(otelConfig),
)

err := retry.Do(ctx, cfg, func(ctx context.Context) error {
	return db.Ping()
})
Example

Do retries the operation with exponential backoff until it succeeds. Here the operation fails twice, then succeeds on the third attempt.

package main

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

	"github.com/jasoet/pkg/v3/retry"
)

func main() {
	cfg := retry.New(
		retry.WithName("flaky.op"),
		retry.WithMaxRetries(5),
		retry.WithInitialInterval(time.Millisecond),
		retry.WithRandomizationFactor(0), // disable jitter for deterministic timing
	)

	attempts := 0
	err := retry.Do(context.Background(), cfg, func(ctx context.Context) error {
		attempts++
		if attempts < 3 {
			return errors.New("temporary failure")
		}
		return nil
	})

	fmt.Println("attempts:", attempts)
	fmt.Println("err:", err)

}
Output:
attempts: 3
err: <nil>

func DoWithNotify

func DoWithNotify(
	ctx context.Context,
	cfg Config,
	operation Operation,
	notifyFunc func(error, time.Duration),
) error

DoWithNotify is like Do but calls notifyFunc on each retry with the error and backoff duration. This is useful for custom logging or metrics.

Example:

err := retry.DoWithNotify(ctx, cfg, operation, func(err error, backoff time.Duration) {
	log.Printf("Retry after error: %v (waiting %v)", err, backoff)
})
Example

DoWithNotify calls the notify function before each retry wait. With RandomizationFactor 0 the backoff durations are exact powers of the multiplier: 10ms, then 20ms.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/jasoet/pkg/v3/retry"
)

func main() {
	cfg := retry.New(
		retry.WithName("notify.op"),
		retry.WithMaxRetries(3),
		retry.WithInitialInterval(10*time.Millisecond),
		retry.WithMultiplier(2.0),
		retry.WithRandomizationFactor(0),
	)

	attempts := 0
	err := retry.DoWithNotify(context.Background(), cfg,
		func(ctx context.Context) error {
			attempts++
			if attempts < 3 {
				return fmt.Errorf("failure #%d", attempts)
			}
			return nil
		},
		func(err error, backoff time.Duration) {
			fmt.Printf("retrying in %v: %v\n", backoff, err)
		},
	)

	fmt.Println("attempts:", attempts)
	fmt.Println("err:", err)

}
Output:
retrying in 10ms: failure #1
retrying in 20ms: failure #2
attempts: 3
err: <nil>

func Permanent

func Permanent(err error) error

Permanent wraps an error to indicate it should not be retried. Use this when an error is not transient and retrying would be pointless.

Example:

return retry.Permanent(fmt.Errorf("invalid configuration"))
Example

Permanent marks an error as non-retryable: Do stops after the first attempt. The returned error wraps the original one, so errors.Is/As still match it.

package main

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

	"github.com/jasoet/pkg/v3/retry"
)

func main() {
	cfg := retry.New(
		retry.WithName("validate.input"),
		retry.WithMaxRetries(5),
		retry.WithInitialInterval(time.Millisecond),
	)

	attempts := 0
	err := retry.Do(context.Background(), cfg, func(ctx context.Context) error {
		attempts++
		return retry.Permanent(errors.New("invalid input"))
	})

	fmt.Println("attempts:", attempts)
	fmt.Println("err:", err)

}
Output:
attempts: 1
err: validate.input failed after 1 attempts (1 initial + 0 retries): invalid input

Types

type Config

type Config struct {
	// MaxRetries is the maximum number of retries after the initial attempt
	// (0 means unlimited retries). With MaxRetries = N the operation is called
	// at most N+1 times: 1 initial attempt plus up to N retries.
	// Default: 5
	MaxRetries uint64 `yaml:"maxRetries" mapstructure:"maxRetries"`

	// InitialInterval is the initial retry interval.
	// Default: 500ms
	InitialInterval time.Duration `yaml:"initialInterval" mapstructure:"initialInterval"`

	// MaxInterval caps the maximum retry interval.
	// Default: 60s
	MaxInterval time.Duration `yaml:"maxInterval" mapstructure:"maxInterval"`

	// Multiplier is the exponential backoff multiplier. Must be > 1.
	// Default: 2.0 (each retry waits 2x longer)
	Multiplier float64 `yaml:"multiplier" mapstructure:"multiplier"`

	// RandomizationFactor adds jitter to backoff intervals to prevent thundering herd.
	// Must be in [0, 1]. 0.0 means no randomization, 0.5 means +/-50% jitter.
	// Default: 0.5
	RandomizationFactor float64 `yaml:"randomizationFactor" mapstructure:"randomizationFactor"`

	// Name is the operation name used for logging and tracing.
	// Default: "retry.operation"
	Name string `yaml:"name" mapstructure:"name"`

	// OTelConfig enables OpenTelemetry tracing and logging.
	// Optional: if nil, no OTel instrumentation.
	OTelConfig *pkgotel.Config `yaml:"-" mapstructure:"-"` // Not serializable from config files
}

Config holds retry configuration with exponential backoff.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a Config with sensible defaults.

func New

func New(opts ...Option) Config

New returns a Config starting from DefaultConfig with each option applied in order.

Example

New starts from DefaultConfig and applies each option in order.

package main

import (
	"fmt"
	"time"

	"github.com/jasoet/pkg/v3/retry"
)

func main() {
	cfg := retry.New(
		retry.WithName("db.connect"),
		retry.WithMaxRetries(3),
		retry.WithInitialInterval(100*time.Millisecond),
	)

	fmt.Println("name:", cfg.Name)
	fmt.Println("maxRetries:", cfg.MaxRetries)
	fmt.Println("initialInterval:", cfg.InitialInterval)
	// Untouched fields keep their defaults:
	fmt.Println("maxInterval:", cfg.MaxInterval)
	fmt.Println("multiplier:", cfg.Multiplier)

}
Output:
name: db.connect
maxRetries: 3
initialInterval: 100ms
maxInterval: 1m0s
multiplier: 2

type Operation

type Operation func(ctx context.Context) error

Operation is a function that may fail and should be retried. Return nil to indicate success, or an error to trigger a retry.

type Option

type Option func(*Config)

Option configures a Config. Options never panic; invalid values are reported as an error by Do and DoWithNotify before the first attempt.

func WithInitialInterval

func WithInitialInterval(interval time.Duration) Option

WithInitialInterval sets the initial retry interval.

func WithMaxInterval

func WithMaxInterval(interval time.Duration) Option

WithMaxInterval sets the maximum retry interval.

func WithMaxRetries

func WithMaxRetries(maxRetries uint64) Option

WithMaxRetries sets the maximum number of retries after the initial attempt.

func WithMultiplier

func WithMultiplier(multiplier float64) Option

WithMultiplier sets the exponential backoff multiplier. Must be > 1; invalid values make Do and DoWithNotify return an error.

func WithName

func WithName(name string) Option

WithName sets the operation name for logging and tracing.

func WithOTelConfig

func WithOTelConfig(otelConfig *pkgotel.Config) Option

WithOTelConfig adds OpenTelemetry configuration to the retry config.

func WithRandomizationFactor

func WithRandomizationFactor(factor float64) Option

WithRandomizationFactor sets the jitter factor for backoff intervals. A value of 0.5 means intervals will vary by +/-50%. Set to 0.0 to disable jitter. Must be in [0, 1]; invalid values make Do and DoWithNotify return an error.

Jump to

Keyboard shortcuts

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