retry

package module
v0.0.0-...-b3e46d2 Latest Latest
Warning

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

Go to latest
Published: Feb 7, 2026 License: MIT Imports: 5 Imported by: 0

README

retry

Go Reference Go Report Card CI codecov

Flexible, composable retry logic for Go with dependency injection support.

Features

  • Dependency Injection — Inject policies at wire-up, customize behavior at call sites
  • Composable Backoff — Chain strategies like Exponential, WithCap, and WithJitter
  • Injectable Clock — Control time in tests without real sleeps
  • Lifecycle Hooks — OnRetry, OnSuccess, OnExhausted for observability
  • Time Budgets — Limit by attempts, total duration, or both
  • Error Aggregation — Collect all errors or just the last one
  • Zero Dependencies — Only the Go standard library

Installation

go get github.com/bjaus/retry

Requires Go 1.25 or later.

Quick Start

package main

import (
    "context"
    "errors"
    "log"
    "time"

    "github.com/bjaus/retry"
)

func main() {
    // Simple one-off retry
    err := retry.Do(context.Background(), func(ctx context.Context) error {
        return callExternalAPI(ctx)
    })
    if err != nil {
        log.Fatal(err)
    }
}

Usage

Creating a Reusable Policy

Policies are created at wire-up time and injected where needed:

// In main or DI container
policy := retry.New(
    retry.WithMaxAttempts(5),
    retry.WithBackoff(retry.Exponential(100*time.Millisecond)),
)

// Inject into services
svc := NewUserService(policy, db)
Customizing at Call Sites

Each call site controls its own retry behavior:

err := policy.Do(ctx, func(ctx context.Context) error {
    return client.Fetch(ctx, id)
},
    retry.If(isTransient),
    retry.OnRetry(func(ctx context.Context, attempt int, err error, delay time.Duration) {
        logger.Warn("retrying", "attempt", attempt, "error", err)
    }),
)
Terminal Errors

Use Stop to signal errors that should not be retried:

func fetchUser(ctx context.Context, id string) (*User, error) {
    user, err := db.Get(ctx, id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, retry.Stop(ErrNotFound)  // Don't retry
    }
    return user, err  // Retry other errors
}
Backoff Strategies

Three base strategies:

retry.Constant(100*time.Millisecond)    // Always 100ms
retry.Linear(100*time.Millisecond)      // 100ms, 200ms, 300ms, ...
retry.Exponential(100*time.Millisecond) // 100ms, 200ms, 400ms, 800ms, ...

Compose with wrappers:

// Exponential, capped at 10s, with ±20% jitter
backoff := retry.WithJitter(0.2,
    retry.WithCap(10*time.Second,
        retry.Exponential(100*time.Millisecond),
    ),
)
Wrapper Description
WithCap(max, b) Caps delay at max duration
WithMin(min, b) Ensures delay is at least min
WithJitter(factor, b) Adds random jitter (±factor × delay)
Time Budgets

Combine attempt limits with duration limits:

policy := retry.New(
    retry.WithMaxAttempts(10),              // Stop after 10 attempts
    retry.WithMaxDuration(30*time.Second),  // OR stop after 30s total
)
Lifecycle Hooks
err := policy.Do(ctx, fn,
    retry.OnRetry(func(ctx context.Context, attempt int, err error, delay time.Duration) {
        logger.Warn("retrying", "attempt", attempt)
        metrics.Increment("retries")
    }),
    retry.OnSuccess(func(ctx context.Context, attempts int) {
        if attempts > 1 {
            logger.Info("recovered", "attempts", attempts)
        }
    }),
    retry.OnExhausted(func(ctx context.Context, attempts int, err error) {
        alerting.Notify("retry exhausted")
    }),
)
Error Aggregation

By default, only the last error is returned:

err := retry.Do(ctx, fn)  // Returns last error only

Collect all errors:

err := retry.Do(ctx, fn, retry.WithAllErrors())
// err contains all attempt errors via errors.Join
// errors.Is/As work through the chain
Pre-Built Policies
retry.Never()   // No retries (max attempts = 1)
retry.Default() // 3 attempts, exponential backoff with jitter

Testing

Inject a fake clock to control time:

type fakeClock struct {
    now    time.Time
    sleeps []time.Duration
}

func (c *fakeClock) Now() time.Time { return c.now }
func (c *fakeClock) Sleep(ctx context.Context, d time.Duration) error {
    c.sleeps = append(c.sleeps, d)
    c.now = c.now.Add(d)
    return ctx.Err()
}

func TestRetry(t *testing.T) {
    clock := &fakeClock{now: time.Now()}
    policy := retry.New(
        retry.WithMaxAttempts(3),
        retry.WithClock(clock),
    )

    attempts := 0
    _ = policy.Do(ctx, func(ctx context.Context) error {
        attempts++
        return errors.New("fail")
    })

    assert.Equal(t, 3, attempts)
    assert.Len(t, clock.sleeps, 2)  // 2 sleeps between 3 attempts
}

API Reference

Policy Options (set at wire-up)
Option Description
WithMaxAttempts(n) Maximum number of attempts
WithMaxDuration(d) Maximum total duration
WithBackoff(b) Backoff strategy
WithClock(c) Clock for time operations (testing)
Call Options (set at each call site)
Option Description
If(cond) Retry if condition returns true
IfNot(cond) Skip retry if condition returns true
Not(cond) Inverts a condition (helper for composing)
OnRetry(fn) Hook called before each retry sleep
OnSuccess(fn) Hook called when function succeeds
OnExhausted(fn) Hook called when all attempts exhausted
WithAllErrors() Collect all errors instead of just the last

Design Philosophy

This package separates configuration into two layers:

Policy-Level (infrastructure controls the budget):

  • How many attempts
  • How long to wait between attempts
  • Total time budget

Call-Level (application controls behavior):

  • Which errors to retry
  • What to log/metric on retry
  • What to do on success/failure

This separation enables clean dependency injection without coupling application code to retry configuration.

License

MIT License - see LICENSE for details.

Documentation

Overview

Package retry provides flexible, composable retry logic with dependency injection support.

retry is a retry package that provides:

  • Dependency Injection: Inject policies at wire-up, customize behavior at call sites
  • Composable Backoff: Chain strategies like Exponential, WithCap, and WithJitter
  • Injectable Clock: Control time in tests without real sleeps
  • Lifecycle Hooks: OnRetry, OnSuccess, OnExhausted for observability
  • Error Aggregation: Collect all errors or just the last one
  • Zero Dependencies: Only the Go standard library

Quick Start

Using the global Do function for one-off retries:

err := retry.Do(ctx, func(ctx context.Context) error {
    return client.Call(ctx)
})

Creating a reusable policy for dependency injection:

// At wire-up time (e.g., in main or a DI container)
policy := retry.New(
    retry.WithMaxAttempts(5),
    retry.WithBackoff(retry.Exponential(100*time.Millisecond)),
)

// At call site
err := policy.Do(ctx, func(ctx context.Context) error {
    return client.Call(ctx)
},
    retry.If(isTransient),
    retry.OnRetry(func(ctx context.Context, attempt int, err error, delay time.Duration) {
        log.Warn("retrying", "attempt", attempt, "error", err, "delay", delay)
    }),
)

Design Philosophy

The package separates configuration into two categories:

Policy-Level (set at wire-up, injected via DI):

  • MaxAttempts: How many times to try
  • MaxDuration: Total time budget across all attempts
  • Backoff: Delay strategy between attempts
  • Clock: Time abstraction for testing

Call-Level (set at each call site):

  • If: Condition to determine if an error should be retried
  • OnRetry: Hook called before each retry sleep
  • OnSuccess: Hook called when the function succeeds
  • OnExhausted: Hook called when all attempts are exhausted
  • WithAllErrors: Collect all errors instead of just the last

This separation allows:

  • Infrastructure to control retry budgets (how many, how fast)
  • Application code to control retry behavior (which errors, what to log)
  • Clean dependency injection without coupling to configuration

Terminal Errors

Use Stop to signal that an error should not be retried:

func fetchUser(ctx context.Context, id string) (*User, error) {
    user, err := db.Get(ctx, id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, retry.Stop(ErrNotFound)  // Don't retry "not found"
    }
    return user, err  // Other errors will be retried
}

Backoff Strategies

The package provides three base strategies:

retry.Constant(100*time.Millisecond)    // Always 100ms
retry.Linear(100*time.Millisecond)      // 100ms, 200ms, 300ms, ...
retry.Exponential(100*time.Millisecond) // 100ms, 200ms, 400ms, 800ms, ...

Strategies can be composed with wrappers:

// Exponential backoff, capped at 10s, with ±20% jitter
backoff := retry.WithJitter(0.2,
    retry.WithCap(10*time.Second,
        retry.Exponential(100*time.Millisecond),
    ),
)

Available wrappers:

  • WithCap(max, b): Caps delay at max duration
  • WithMin(min, b): Ensures delay is at least min duration
  • WithJitter(factor, b): Adds random jitter (±factor * delay)

Custom backoff strategies can be created using BackoffFunc:

custom := retry.BackoffFunc(func(attempt int) time.Duration {
    return time.Duration(attempt*attempt) * 100 * time.Millisecond
})

Time Budgets

Use both MaxAttempts and MaxDuration for precise control:

policy := retry.New(
    retry.WithMaxAttempts(10),               // Stop after 10 attempts
    retry.WithMaxDuration(30*time.Second),   // OR stop after 30s total
)

The retry loop stops when either limit is reached first.

Lifecycle Hooks

Hooks provide observability without coupling to a specific logger or metrics system:

err := policy.Do(ctx, fn,
    retry.OnRetry(func(ctx context.Context, attempt int, err error, delay time.Duration) {
        logger.Warn("retrying", "attempt", attempt, "delay", delay)
        metrics.Increment("retries")
    }),
    retry.OnSuccess(func(ctx context.Context, attempts int) {
        if attempts > 1 {
            logger.Info("recovered", "attempts", attempts)
        }
    }),
    retry.OnExhausted(func(ctx context.Context, attempts int, err error) {
        logger.Error("gave up", "attempts", attempts, "error", err)
        alerting.Notify("retry exhausted")
    }),
)

Error Aggregation

By default, only the last error is returned. Use WithAllErrors to collect all:

err := retry.Do(ctx, fn, retry.WithAllErrors())
// err contains all attempt errors via errors.Join
// errors.Is/As work through the chain

Testing

Inject a fake clock to control time in tests:

type fakeClock struct {
    now    time.Time
    sleeps []time.Duration
}

func (c *fakeClock) Now() time.Time { return c.now }
func (c *fakeClock) Sleep(ctx context.Context, d time.Duration) error {
    c.sleeps = append(c.sleeps, d)
    c.now = c.now.Add(d)
    return ctx.Err()
}

func TestRetry(t *testing.T) {
    clock := &fakeClock{now: time.Now()}
    policy := retry.New(
        retry.WithMaxAttempts(3),
        retry.WithClock(clock),
    )

    attempts := 0
    _ = policy.Do(ctx, func(ctx context.Context) error {
        attempts++
        return errors.New("fail")
    })

    assert.Equal(t, 3, attempts)
    assert.Len(t, clock.sleeps, 2) // 2 sleeps between 3 attempts
}

Pre-Built Policies

The package provides convenience functions for common configurations:

retry.Never()   // No retries, just run once
retry.Default() // Sensible defaults (3 attempts, exponential backoff with jitter)

Best Practices

1. Inject policies, customize at call sites:

// Wire-up
policy := retry.New(retry.WithMaxAttempts(5))

// Call site
err := policy.Do(ctx, fn, retry.If(isTransient))

2. Use Stop for non-retryable errors:

if errors.Is(err, ErrNotFound) {
    return retry.Stop(err)
}

3. Add jitter to prevent thundering herd:

retry.WithJitter(0.2, retry.Exponential(100*time.Millisecond))

4. Cap exponential backoff to prevent excessive delays:

retry.WithCap(30*time.Second, retry.Exponential(100*time.Millisecond))

5. Use hooks for observability instead of wrapping:

retry.OnRetry(func(...) { logger.Warn(...) })
Example (ComposedBackoff)

Example_composedBackoff demonstrates composing multiple backoff wrappers.

package main

import (
	"fmt"
	"time"

	"github.com/bjaus/retry"
)

func main() {
	// Exponential backoff, capped at 1s, with minimum 50ms
	b := retry.WithMin(50*time.Millisecond,
		retry.WithCap(1*time.Second,
			retry.Exponential(10*time.Millisecond),
		),
	)

	fmt.Println("Attempt 1:", b.Delay(1))   // 10ms -> 50ms (min)
	fmt.Println("Attempt 2:", b.Delay(2))   // 20ms -> 50ms (min)
	fmt.Println("Attempt 5:", b.Delay(5))   // 160ms
	fmt.Println("Attempt 10:", b.Delay(10)) // 5.12s -> 1s (cap)

}
Output:

Attempt 1: 50ms
Attempt 2: 50ms
Attempt 5: 160ms
Attempt 10: 1s
Example (DependencyInjection)

Example_dependencyInjection demonstrates the recommended DI pattern.

package main

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

	"github.com/bjaus/retry"
)

func main() {
	// === Wire-up time (e.g., in main or DI container) ===
	policy := retry.New(
		retry.WithMaxAttempts(5),
		retry.WithBackoff(retry.Constant(time.Millisecond)),
	)

	// === Call site (in application code) ===
	// The caller doesn't know or care about the retry budget.
	// It only controls which errors to retry and what to log.
	attempts := 0
	var retried bool

	err := policy.Do(context.Background(), func(ctx context.Context) error {
		attempts++
		if attempts < 2 {
			return errors.New("transient")
		}
		return nil
	},
		retry.If(func(err error) bool {
			return err.Error() == "transient"
		}),
		retry.OnRetry(func(ctx context.Context, attempt int, err error, delay time.Duration) {
			retried = true
		}),
	)

	fmt.Println("Error:", err)
	fmt.Println("Retried:", retried)

}
Output:

Error: <nil>
Retried: true

Index

Examples

Constants

View Source
const (
	DefaultMaxAttempts = 3
)

Default values.

Variables

This section is empty.

Functions

func Do

func Do(ctx context.Context, fn Func, opts ...Option) error

Do executes fn with retry using the default policy.

Example

ExampleDo demonstrates the simplest usage with the global Do function.

package main

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

	"github.com/bjaus/retry"
)

func main() {
	attempts := 0
	err := retry.Do(context.Background(), func(ctx context.Context) error {
		attempts++
		if attempts < 3 {
			return errors.New("temporary failure")
		}
		return nil
	},
		retry.WithMaxAttempts(5),
		retry.WithBackoff(retry.Constant(time.Millisecond)),
	)

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

}
Output:

Error: <nil>
Attempts: 3

func Stop

func Stop(err error) error

Stop wraps an error to signal that it should not be retried. The retry loop will immediately return the unwrapped error.

Example

ExampleStop demonstrates signaling a non-retryable error.

package main

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

	"github.com/bjaus/retry"
)

func main() {
	notFound := errors.New("not found")

	attempts := 0
	err := retry.Do(context.Background(), func(ctx context.Context) error {
		attempts++
		return retry.Stop(notFound)
	},
		retry.WithMaxAttempts(5),
		retry.WithBackoff(retry.Constant(time.Millisecond)),
	)

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

}
Output:

Error: not found
Attempts: 1

Types

type Backoff

type Backoff interface {
	Delay(attempt int) time.Duration
}

Backoff calculates the delay between retry attempts.

func Constant

func Constant(d time.Duration) Backoff

Constant returns a backoff that always waits the same duration.

Example

ExampleConstant demonstrates constant backoff.

package main

import (
	"fmt"
	"time"

	"github.com/bjaus/retry"
)

func main() {
	b := retry.Constant(100 * time.Millisecond)

	fmt.Println("Attempt 1:", b.Delay(1))
	fmt.Println("Attempt 2:", b.Delay(2))
	fmt.Println("Attempt 5:", b.Delay(5))

}
Output:

Attempt 1: 100ms
Attempt 2: 100ms
Attempt 5: 100ms

func Exponential

func Exponential(base time.Duration) Backoff

Exponential returns a backoff that doubles with each attempt. delay = base * 2^(attempt-1)

Example

ExampleExponential demonstrates exponential backoff.

package main

import (
	"fmt"
	"time"

	"github.com/bjaus/retry"
)

func main() {
	b := retry.Exponential(100 * time.Millisecond)

	fmt.Println("Attempt 1:", b.Delay(1))
	fmt.Println("Attempt 2:", b.Delay(2))
	fmt.Println("Attempt 3:", b.Delay(3))
	fmt.Println("Attempt 4:", b.Delay(4))

}
Output:

Attempt 1: 100ms
Attempt 2: 200ms
Attempt 3: 400ms
Attempt 4: 800ms

func Linear

func Linear(base time.Duration) Backoff

Linear returns a backoff that increases linearly with each attempt. delay = base * attempt

Example

ExampleLinear demonstrates linear backoff.

package main

import (
	"fmt"
	"time"

	"github.com/bjaus/retry"
)

func main() {
	b := retry.Linear(100 * time.Millisecond)

	fmt.Println("Attempt 1:", b.Delay(1))
	fmt.Println("Attempt 2:", b.Delay(2))
	fmt.Println("Attempt 5:", b.Delay(5))

}
Output:

Attempt 1: 100ms
Attempt 2: 200ms
Attempt 5: 500ms

func WithCap

func WithCap(max time.Duration, b Backoff) Backoff

WithCap wraps a backoff and caps the delay at a maximum value.

Example

ExampleWithCap demonstrates capping backoff delays.

package main

import (
	"fmt"
	"time"

	"github.com/bjaus/retry"
)

func main() {
	b := retry.WithCap(500*time.Millisecond, retry.Exponential(100*time.Millisecond))

	fmt.Println("Attempt 1:", b.Delay(1))
	fmt.Println("Attempt 2:", b.Delay(2))
	fmt.Println("Attempt 3:", b.Delay(3))
	fmt.Println("Attempt 4:", b.Delay(4)) // Would be 800ms, capped to 500ms
	fmt.Println("Attempt 5:", b.Delay(5)) // Would be 1.6s, capped to 500ms

}
Output:

Attempt 1: 100ms
Attempt 2: 200ms
Attempt 3: 400ms
Attempt 4: 500ms
Attempt 5: 500ms

func WithJitter

func WithJitter(factor float64, b Backoff) Backoff

WithJitter wraps a backoff and adds random jitter to the delay. The jitter is a factor between 0 and 1, where 0.2 means ±20%.

func WithMin

func WithMin(min time.Duration, b Backoff) Backoff

WithMin wraps a backoff and ensures the delay is at least a minimum value.

Example

ExampleWithMin demonstrates minimum backoff delays.

package main

import (
	"fmt"
	"time"

	"github.com/bjaus/retry"
)

func main() {
	b := retry.WithMin(150*time.Millisecond, retry.Linear(50*time.Millisecond))

	fmt.Println("Attempt 1:", b.Delay(1)) // 50ms -> 150ms (min)
	fmt.Println("Attempt 2:", b.Delay(2)) // 100ms -> 150ms (min)
	fmt.Println("Attempt 3:", b.Delay(3)) // 150ms (at min)
	fmt.Println("Attempt 4:", b.Delay(4)) // 200ms (above min)

}
Output:

Attempt 1: 150ms
Attempt 2: 150ms
Attempt 3: 150ms
Attempt 4: 200ms

type BackoffFunc

type BackoffFunc func(attempt int) time.Duration

BackoffFunc is an adapter that allows a function to be used as a Backoff.

Example

ExampleBackoffFunc demonstrates creating a custom backoff strategy.

package main

import (
	"fmt"
	"time"

	"github.com/bjaus/retry"
)

func main() {
	// Quadratic backoff: delay = base * attempt^2
	b := retry.BackoffFunc(func(attempt int) time.Duration {
		return time.Duration(attempt*attempt) * 10 * time.Millisecond
	})

	fmt.Println("Attempt 1:", b.Delay(1))
	fmt.Println("Attempt 2:", b.Delay(2))
	fmt.Println("Attempt 3:", b.Delay(3))
	fmt.Println("Attempt 4:", b.Delay(4))

}
Output:

Attempt 1: 10ms
Attempt 2: 40ms
Attempt 3: 90ms
Attempt 4: 160ms

func (BackoffFunc) Delay

func (f BackoffFunc) Delay(attempt int) time.Duration

Delay implements Backoff.

type Clock

type Clock interface {
	Now() time.Time
	Sleep(ctx context.Context, d time.Duration) error
}

Clock abstracts time operations for testing.

type Condition

type Condition func(error) bool

Condition determines whether an error should be retried.

func Not

func Not(cond Condition) Condition

Not inverts a condition.

Example

ExampleNot demonstrates inverting a condition.

package main

import (
	"errors"
	"fmt"

	"github.com/bjaus/retry"
)

func main() {
	isTimeout := func(err error) bool {
		return err.Error() == "timeout"
	}

	// Retry everything EXCEPT timeouts
	notTimeout := retry.Not(isTimeout)

	fmt.Println("timeout matches:", isTimeout(errors.New("timeout")))
	fmt.Println("timeout matches Not:", notTimeout(errors.New("timeout")))
	fmt.Println("other matches Not:", notTimeout(errors.New("other")))

}
Output:

timeout matches: true
timeout matches Not: false
other matches Not: true

type Func

type Func func(ctx context.Context) error

Func is the function signature for retryable operations.

type OnExhaustedFunc

type OnExhaustedFunc func(ctx context.Context, attempts int, err error)

OnExhaustedFunc is called when all retry attempts are exhausted.

type OnRetryFunc

type OnRetryFunc func(ctx context.Context, attempt int, err error, delay time.Duration)

OnRetryFunc is called before each retry sleep.

type OnSuccessFunc

type OnSuccessFunc func(ctx context.Context, attempts int)

OnSuccessFunc is called when the function succeeds.

type Option

type Option func(*config)

Option configures retry behavior.

func If

func If(cond Condition) Option

If sets the condition that determines whether an error should be retried. If the condition returns false, the retry loop stops immediately.

Example

ExampleIf demonstrates conditional retry based on error type.

package main

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

	"github.com/bjaus/retry"
)

func main() {
	transient := errors.New("transient error")
	permanent := errors.New("permanent error")

	attempts := 0
	err := retry.Do(context.Background(), func(ctx context.Context) error {
		attempts++
		if attempts < 3 {
			return transient
		}
		return permanent
	},
		retry.WithMaxAttempts(10),
		retry.WithBackoff(retry.Constant(time.Millisecond)),
		retry.If(func(err error) bool {
			return errors.Is(err, transient)
		}),
	)

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

}
Output:

Error: permanent error
Attempts: 3

func IfNot

func IfNot(cond Condition) Option

IfNot sets a condition where matching errors are NOT retried. This is equivalent to If(Not(cond)).

Example

ExampleIfNot demonstrates skipping specific errors from retry.

package main

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

	"github.com/bjaus/retry"
)

func main() {
	validationErr := errors.New("validation error")

	attempts := 0
	err := retry.Do(context.Background(), func(ctx context.Context) error {
		attempts++
		if attempts == 2 {
			return validationErr // Don't retry this
		}
		return errors.New("transient")
	},
		retry.WithMaxAttempts(10),
		retry.WithBackoff(retry.Constant(time.Millisecond)),
		retry.IfNot(func(err error) bool {
			return errors.Is(err, validationErr)
		}),
	)

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

}
Output:

Error: validation error
Attempts: 2

func OnExhausted

func OnExhausted(fn OnExhaustedFunc) Option

OnExhausted sets a hook that is called when all retry attempts are exhausted.

Example

ExampleOnExhausted demonstrates the exhausted hook.

package main

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

	"github.com/bjaus/retry"
)

func main() {
	_ = retry.Do(context.Background(), func(ctx context.Context) error {
		return errors.New("always fails")
	},
		retry.WithMaxAttempts(3),
		retry.WithBackoff(retry.Constant(time.Millisecond)),
		retry.OnExhausted(func(ctx context.Context, attempts int, err error) {
			fmt.Printf("Exhausted after %d attempts: %v\n", attempts, err)
		}),
	)

}
Output:

Exhausted after 3 attempts: always fails

func OnRetry

func OnRetry(fn OnRetryFunc) Option

OnRetry sets a hook that is called before each retry sleep.

Example

ExampleOnRetry demonstrates the retry hook for logging.

package main

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

	"github.com/bjaus/retry"
)

func main() {
	attempts := 0
	retryCount := 0

	_ = retry.Do(context.Background(), func(ctx context.Context) error {
		attempts++
		return errors.New("fail")
	},
		retry.WithMaxAttempts(3),
		retry.WithBackoff(retry.Constant(time.Millisecond)),
		retry.OnRetry(func(ctx context.Context, attempt int, err error, delay time.Duration) {
			retryCount++
			fmt.Printf("Retry %d: %v\n", attempt, err)
		}),
	)

	fmt.Println("Total retries:", retryCount)

}
Output:

Retry 1: fail
Retry 2: fail
Total retries: 2

func OnSuccess

func OnSuccess(fn OnSuccessFunc) Option

OnSuccess sets a hook that is called when the function succeeds.

Example

ExampleOnSuccess demonstrates the success hook.

package main

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

	"github.com/bjaus/retry"
)

func main() {
	attempts := 0

	_ = retry.Do(context.Background(), func(ctx context.Context) error {
		attempts++
		if attempts < 3 {
			return errors.New("not yet")
		}
		return nil
	},
		retry.WithMaxAttempts(5),
		retry.WithBackoff(retry.Constant(time.Millisecond)),
		retry.OnSuccess(func(ctx context.Context, attempts int) {
			fmt.Printf("Succeeded on attempt %d\n", attempts)
		}),
	)

}
Output:

Succeeded on attempt 3

func WithAllErrors

func WithAllErrors() Option

WithAllErrors configures the retry to collect all errors from each attempt. When enabled, the final error is an errors.Join of all attempt errors. By default, only the last error is returned.

Example

ExampleWithAllErrors demonstrates collecting all errors.

package main

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

	"github.com/bjaus/retry"
)

func main() {
	attempt := 0
	err := retry.Do(context.Background(), func(ctx context.Context) error {
		attempt++
		return fmt.Errorf("error %d", attempt)
	},
		retry.WithMaxAttempts(3),
		retry.WithBackoff(retry.Constant(time.Millisecond)),
		retry.WithAllErrors(),
	)

	fmt.Println("Contains error 1:", errors.Is(err, fmt.Errorf("error 1")))
	fmt.Println("Error string contains all:", err != nil)

}
Output:

Contains error 1: false
Error string contains all: true

func WithBackoff

func WithBackoff(b Backoff) Option

WithBackoff sets the backoff strategy.

func WithClock

func WithClock(clock Clock) Option

WithClock sets the clock for time operations. Useful for testing.

func WithMaxAttempts

func WithMaxAttempts(n int) Option

WithMaxAttempts sets the maximum number of attempts.

func WithMaxDuration

func WithMaxDuration(d time.Duration) Option

WithMaxDuration sets the maximum total duration for all attempts. Retries stop when this duration is exceeded, even if attempts remain.

type Policy

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

Policy defines retry behavior. Safe for concurrent use.

func Default

func Default() *Policy

Default returns a policy with sensible defaults.

func Never

func Never() *Policy

Never returns a policy that does not retry.

Example

ExampleNever demonstrates a policy that does not retry.

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/bjaus/retry"
)

func main() {
	policy := retry.Never()

	attempts := 0
	_ = policy.Do(context.Background(), func(ctx context.Context) error {
		attempts++
		return errors.New("fail")
	})

	fmt.Println("Attempts:", attempts)

}
Output:

Attempts: 1

func New

func New(opts ...Option) *Policy

New creates a Policy with the given options.

Example

ExampleNew demonstrates creating a reusable policy.

package main

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

	"github.com/bjaus/retry"
)

func main() {
	policy := retry.New(
		retry.WithMaxAttempts(3),
		retry.WithBackoff(retry.Constant(time.Millisecond)),
	)

	attempts := 0
	err := policy.Do(context.Background(), func(ctx context.Context) error {
		attempts++
		return errors.New("always fails")
	})

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

}
Output:

Error: always fails
Attempts: 3

func (*Policy) Do

func (p *Policy) Do(ctx context.Context, fn Func, opts ...Option) error

Do executes fn with retry using this policy's configuration.

Jump to

Keyboard shortcuts

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