wrap

package
v0.111.0 Latest Latest
Warning

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

Go to latest
Published: Mar 27, 2026 License: MIT Imports: 6 Imported by: 1

README

wrap

Resilience decorators for context-aware functions. Wrap a function with retry and circuit breaking — chainable methods, no type arguments needed.

safeFetch := wrap.Func(fetchUser).
    Retry(3, wrap.ExpBackoff(time.Second), nil).
    Breaker(breaker)

What It Looks Like

// Circuit breaker — trips after 5 consecutive failures, resets after 30s
breaker := wrap.NewBreaker(wrap.BreakerConfig{
    ResetTimeout: 30 * time.Second,
    ReadyToTrip:  wrap.ConsecutiveFailures(5),
})
safeFetch := wrap.Func(fetchFromAPI).Breaker(breaker)
resp, err := safeFetch(ctx, url)  // returns wrap.ErrCircuitOpen when tripped
// Retry transient errors, then circuit-break the dependency
resilient := wrap.Func(fetchData).
    Retry(3, wrap.ExpBackoff(100*time.Millisecond), isTransient).
    Breaker(breaker)
// Error observation and transformation
observed := wrap.Func(fetchUser).OnError(logErr).MapError(annotate)
// Custom decorators via Apply
wrap.Func(fn).Retry(3, backoff, nil).Apply(myCustomDecorator)

Methods on Fn[T, R]

Method Purpose
Breaker(b) Circuit breaker — shared *Breaker from NewBreaker(cfg)
MapError(mapper) Transform errors via mapping function
OnError(handler) Side-effect handler on error
Retry(max, backoff, pred) Retry on error with backoff strategy
Apply(ds...) Apply custom Decorator values

Backoff

ExpBackoff(initial) — randomized exponential: uniform random in [0, initial * 2^n). Spreads retries to minimize collisions under contention.

Circuit Breaker

  • NewBreaker(cfg) *Breaker — 3-state: closed → open → half-open → closed
  • ConsecutiveFailures(n) func(Snapshot) bool — ReadyToTrip predicate
  • ErrCircuitOpen — sentinel error when breaker rejects

All context-aware decorators return ctx.Err() on cancellation. Circuit breaker does not count context.Canceled as a failure.

See pkg.go.dev for complete API documentation.

Documentation

Overview

Package wrap provides chainable decorators for context-aware effectful functions.

Start with Func to wrap a plain function, then chain With* methods:

safe := wrap.Func(fetchOrder).
    WithRetry(3, wrap.ExpBackoff(time.Second), nil).
    WithBreaker(breaker).
    WithThrottle(10)

Each method returns Fn, preserving the func(context.Context, T) (R, error) signature so decorators compose freely. For custom decorators, use [Fn.With] with Decorator values.

For higher-order functions over plain signatures — func(A) B composition, partial application, debouncing — see the [hof] package.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrCircuitOpen = errors.New("call: circuit breaker is open")

ErrCircuitOpen is returned when the circuit breaker is rejecting requests. This occurs when the breaker is open, or when half-open with a probe already in flight.

Functions

func ConsecutiveFailures

func ConsecutiveFailures(n int) func(Snapshot) bool

ConsecutiveFailures returns a ReadyToTrip predicate that trips after n consecutive failures. Panics if n < 1.

Types

type Backoff

type Backoff func(n int) time.Duration

Backoff computes the delay before retry number n (0-indexed). Called between attempts: backoff(0) is the delay before the first retry.

func ExpBackoff

func ExpBackoff(initial time.Duration) Backoff

ExpBackoff returns a randomized exponential Backoff: uniform random in [0, initial * 2^n). Spreads retries across the interval to minimize collisions under contention. Panics if initial <= 0.

type Breaker

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

Breaker is a circuit breaker that tracks failures and short-circuits requests when a dependency is unhealthy. Use NewBreaker to create and WithBreaker to wrap functions for composition with Retry, Throttle, and other hof wrappers.

The breaker uses a standard three-state model:

  • Closed: requests pass through, failures are counted
  • Open: requests fail immediately with ErrCircuitOpen
  • HalfOpen: one probe request is admitted; success closes, failure reopens, uncounted error (context.Canceled or ShouldCount→false) releases the probe slot without changing state

State transitions are lazy (checked on admission, not timer-driven). One probe request is admitted in half-open; all others are rejected.

Each state transition increments an internal generation counter. Calls that complete after the breaker has moved to a new generation are silently ignored, preventing stale in-flight results from corrupting the current epoch's metrics.

A Breaker must represent a single dependency or failure domain. Sharing a breaker across unrelated dependencies causes pathological coupling: one dependency's failures can trip the breaker for all, and one dependency's successful probe can close it while others remain unhealthy.

func NewBreaker

func NewBreaker(cfg BreakerConfig) *Breaker

NewBreaker creates a circuit breaker with the given configuration. Panics if ResetTimeout <= 0.

func (*Breaker) Snapshot

func (b *Breaker) Snapshot() Snapshot

Snapshot returns a point-in-time view of the breaker's state and metrics. State is the committed state; lazy transitions (open to half-open after resetTimeout) are not reflected until the next admission check.

type BreakerConfig

type BreakerConfig struct {
	// ResetTimeout is how long the breaker stays open before allowing a probe request.
	// Must be > 0.
	ResetTimeout time.Duration

	// ReadyToTrip decides whether the breaker should open based on current metrics.
	// Called outside the internal lock after each counted failure while closed.
	// The snapshot reflects the state including the current failure.
	//
	// Under concurrency, the breaker validates that no metric mutations occurred
	// between ReadyToTrip evaluation and the trip commit. If metrics changed
	// (concurrent success or failure), the trip is aborted; the next failure
	// will re-evaluate with a fresh snapshot. This means predicates should be
	// monotone with respect to failure accumulation (e.g., >= threshold) for
	// reliable tripping under contention. Non-monotone predicates (e.g., == N)
	// may miss a trip if a concurrent mutation changes the count between
	// evaluation and commit.
	//
	// Must be side-effect-free. May be called concurrently from multiple goroutines.
	// Nil defaults to ConsecutiveFailures(5).
	ReadyToTrip func(Snapshot) bool

	// ShouldCount decides whether an error counts as a failure for trip purposes.
	// Called outside the internal lock.
	// Nil means all errors count. context.Canceled never counts regardless of this setting.
	ShouldCount func(error) bool

	// OnStateChange is called after each state transition on the normal (non-panic) path,
	// outside the internal lock. Transitions caused by panic recovery (e.g., a half-open
	// probe fn panic reopening the breaker) do not trigger the callback to avoid masking
	// the original panic.
	// Under concurrency, callback delivery may lag or overlap and should not
	// be treated as a total order. Panics in this callback propagate to the caller.
	// Nil means no notification.
	OnStateChange func(Transition)

	// Clock returns the current time. Nil defaults to time.Now.
	// Must be non-blocking, must not panic, and must not call Breaker methods
	// (deadlock risk). Useful for deterministic testing.
	Clock func() time.Time
}

BreakerConfig configures a circuit breaker.

type BreakerState

type BreakerState int

BreakerState represents the current state of a circuit breaker.

const (
	StateClosed BreakerState = iota
	StateOpen
	StateHalfOpen
)

func (BreakerState) String

func (s BreakerState) String() string

type Decorator

type Decorator[T, R any] func(Fn[T, R]) Fn[T, R]

Decorator wraps an Fn, returning an Fn with the same signature. Use with Fn.Apply for custom decorators.

type Fn

type Fn[T, R any] func(context.Context, T) (R, error)

Fn is the function shape all decorators operate on: a context-aware function that returns a value or an error.

Example (Chain)
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/binaryphile/fluentfp/wrap"
)

func main() {
	// fetchData simulates a remote call.
	fetchData := func(_ context.Context, key string) (string, error) {
		return fmt.Sprintf("data(%s)", key), nil
	}

	breaker := wrap.NewBreaker(wrap.BreakerConfig{
		ResetTimeout: 10 * time.Second,
	})

	// Retry transient errors, then circuit-break the dependency.
	resilient := wrap.Func(fetchData).
		Retry(3, wrap.ExpBackoff(time.Millisecond), nil).
		Breaker(breaker)

	got, _ := resilient(context.Background(), "abc")
	fmt.Println(got)
}
Output:
data(abc)

func Func

func Func[T, R any](fn func(context.Context, T) (R, error)) Fn[T, R]

Func wraps a plain function as an Fn for fluent decoration. Go infers the type parameters from fn:

wrap.Func(fetchUser).
    Retry(3, wrap.ExpBackoff(time.Second), nil).
    Breaker(breaker)

func (Fn[T, R]) Apply added in v0.109.0

func (f Fn[T, R]) Apply(ds ...Decorator[T, R]) Fn[T, R]

Apply applies custom decorators to f in order (innermost-first).

func (Fn[T, R]) Breaker added in v0.109.0

func (f Fn[T, R]) Breaker(b *Breaker) Fn[T, R]

Breaker wraps f with circuit breaker protection. The breaker is shared state — pass the same *Breaker to multiple wrapped functions to have them trip together.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/binaryphile/fluentfp/wrap"
)

func main() {
	// double doubles the input.
	double := func(_ context.Context, n int) (int, error) { return n * 2, nil }

	breaker := wrap.NewBreaker(wrap.BreakerConfig{
		ResetTimeout: 10 * time.Second,
	})

	protected := wrap.Func(double).Breaker(breaker)
	got, _ := protected(context.Background(), 21)
	fmt.Println(got)
}
Output:
42

func (Fn[T, R]) MapError added in v0.109.0

func (f Fn[T, R]) MapError(mapper func(error) error) Fn[T, R]

MapError wraps f so that any non-nil error is transformed by mapper.

Example
package main

import (
	"context"
	"fmt"

	"github.com/binaryphile/fluentfp/wrap"
)

func main() {
	// fetchUser simulates a user lookup that fails.
	fetchUser := func(_ context.Context, id int) (string, error) {
		return "", fmt.Errorf("not found")
	}

	// annotate wraps errors with calling context.
	annotate := func(err error) error {
		return fmt.Errorf("fetchUser(%d): %w", 42, err)
	}

	wrapped := wrap.Func(fetchUser).MapError(annotate)
	_, err := wrapped(context.Background(), 42)
	fmt.Println(err)
}
Output:
fetchUser(42): not found

func (Fn[T, R]) OnError added in v0.109.0

func (f Fn[T, R]) OnError(handler func(error)) Fn[T, R]

OnError wraps f so that handler is called on non-nil errors. The error is not modified.

Example
package main

import (
	"context"
	"fmt"

	"github.com/binaryphile/fluentfp/wrap"
)

func main() {
	// fetchUser simulates a user lookup that fails.
	fetchUser := func(_ context.Context, id int) (string, error) {
		return "", fmt.Errorf("not found")
	}

	// logError prints the error without changing the return value.
	logError := func(err error) {
		fmt.Printf("logged: %v\n", err)
	}

	observed := wrap.Func(fetchUser).OnError(logError)
	_, err := observed(context.Background(), 1)
	fmt.Printf("returned: %v\n", err)
}
Output:
logged: not found
returned: not found

func (Fn[T, R]) Retry added in v0.109.0

func (f Fn[T, R]) Retry(max int, backoff Backoff, shouldRetry func(error) bool) Fn[T, R]

Retry wraps f to retry on error up to max total attempts.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/binaryphile/fluentfp/wrap"
)

func main() {
	// double doubles the input. Succeeds on first try.
	double := func(_ context.Context, n int) (int, error) { return n * 2, nil }

	resilient := wrap.Func(double).Retry(3, wrap.ExpBackoff(time.Millisecond), nil)

	got, _ := resilient(context.Background(), 5)
	fmt.Println(got)
}
Output:
10

type Snapshot

type Snapshot struct {
	State               BreakerState
	Successes           int
	Failures            int
	ConsecutiveFailures int
	Rejected            int
	OpenedAt            time.Time
}

Snapshot is a point-in-time view of breaker state and metrics. Successes and Failures reset when the breaker transitions to closed. ConsecutiveFailures resets on any success (including while closed). Rejected is a lifetime counter. OpenedAt is the zero time when State is StateClosed.

type Transition

type Transition struct {
	From BreakerState
	To   BreakerState
	At   time.Time
}

Transition describes a circuit breaker state change.

Jump to

Keyboard shortcuts

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