call

package
v0.59.0 Latest Latest
Warning

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

Go to latest
Published: Mar 21, 2026 License: MIT Imports: 6 Imported by: 0

README

call

Resilience decorators for communicating with runtime dependencies. Named after effectful call decorators — communication over unreliable channels. "Breaker, breaker."

All decorators wrap func(context.Context, T) (R, error) and return the same signature, so they compose by stacking. Use Func.With to build a stack in one expression:

safeFetch := call.From(fetchUser).With(
    call.CircuitBreaker(breaker),
    call.Retrier(3, backoff, isTransient),
    call.ErrMapper(classifyError),
)

Or apply one at a time with the direct wrappers:

classified := call.MapErr(fetchUser, classifyError)
retried := call.Retry(3, backoff, isTransient, classified)
safeFetch := call.WithBreaker(breaker, retried)

What It Looks Like

// Retry with exponential backoff, only for transient errors
backoff := call.ExponentialBackoff(100 * time.Millisecond)
fetcher := call.Retry(3, backoff, isTransient, fetchData)
// Circuit breaker — trips after 5 consecutive failures, resets after 30s
breaker := call.NewBreaker(call.BreakerConfig{
    ResetTimeout: 30 * time.Second,
    ReadyToTrip:  call.ConsecutiveFailures(5),
})
safeFetch := call.WithBreaker(breaker, fetchFromAPI)
resp, err := safeFetch(ctx, url)  // returns call.ErrOpen when tripped
// Bound concurrency — at most 5 in-flight API calls
callAPI := call.Throttle(5, fetchFromAPI)
// Bound by total cost — large items consume more budget
fetchData := call.ThrottleWeighted(100, estimateSize, fetchFromAPI)
// Cancel remaining work on first error
failFast := call.OnErr(fetchURL, func(_ error) { cancel() })
// Transform errors without changing the function signature
annotated := call.MapErr(fetchUser, classifyError)
// Debounce rapid calls, execute once after quiet period
d := call.NewDebouncer(500*time.Millisecond, saveConfig)
defer d.Close()
d.Call(cfg)

Operations

Circuit Breaking

  • NewBreaker(cfg BreakerConfig) *Breaker — 3-state: closed → open → half-open → closed
  • WithBreaker[T, R](b *Breaker, fn) fn — wrap fn with breaker protection
  • ConsecutiveFailures(n int) func(Snapshot) bool — ReadyToTrip predicate
  • ErrOpen — sentinel error when breaker rejects

Retry

  • Retry[T, R](maxAttempts, backoff, shouldRetry, fn) fn — retry on error with pluggable backoff
  • ConstantBackoff(delay) Backoff — fixed delay
  • ExponentialBackoff(initial) Backoff — full jitter: random in [0, initial * 2^n)

Concurrency Control

  • Throttle[T, R](n, fn) fn — bound by call count
  • ThrottleWeighted[T, R](capacity, cost, fn) fn — bound by total cost

Side-Effect Wrappers

  • OnErr[T, R](fn, onErr) fn — call handler on error
  • MapErr[T, R](fn, mapper) fn — transform errors

Debounce

  • NewDebouncer[T](wait, fn, opts...) *Debouncer[T] — trailing-edge coalescer
  • MaxWait(d) DebounceOption — cap maximum deferral

All context-aware wrappers return ctx.Err() on cancellation. WithBreaker does not count context.Canceled as a failure. All functions panic on nil inputs.

See pkg.go.dev for complete API documentation and the orders example for a full integration demo.

Documentation

Overview

Package call provides decorators for context-aware effectful functions.

Every decorator in this package wraps func(context.Context, T) (R, error) and returns the same signature. This uniform shape is the organizing principle: decorators compose by stacking because the types match at every layer.

For higher-order functions over plain signatures — func(A) B composition, partial application, debouncing — see the [hof] package. The seam between call and hof is the function signature: call operates on the context-aware error-returning call shape; hof operates on everything else.

Index

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.

func MapErr

func MapErr[T, R any](fn func(context.Context, T) (R, error), mapper func(error) error) func(context.Context, T) (R, error)

MapErr wraps fn so that any non-nil error returned by fn is transformed by mapper before being returned. The result value from fn is always preserved unchanged.

Example — annotate errors from a repository call:

// annotateGetUser wraps err with get-user calling context.
annotateGetUser := func(err error) error {
    return fmt.Errorf("get user: %w", err)
}
annotated := call.MapErr(repo.GetUser, annotateGetUser)

mapper is only called for non-nil errors. For any non-nil input, mapper must return a non-nil error; the returned function panics otherwise because MapErr cannot safely convert failure into success — the wrapped function may not define a meaningful result on error.

Composition order matters: the outer wrapper sees the inner wrapper's returned error. Use fmt.Errorf with %w to preserve error identity.

Panics at construction time if fn is nil or mapper is nil.

func OnErr

func OnErr[T, R any](fn func(context.Context, T) (R, error), onErr func(error)) func(context.Context, T) (R, error)

OnErr wraps fn so that onErr is called with the error after fn returns a non-nil error. The returned function calls fn, checks for error, calls onErr(err) if present, then returns fn's original results unchanged.

onErr must be safe for concurrent use when the returned function is called from multiple goroutines.

Panics if fn is nil or onErr is nil.

func Retry

func Retry[T, R any](maxAttempts int, backoff Backoff, shouldRetry func(error) bool, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)

Retry wraps fn to retry on error up to maxAttempts total times. The first call is immediate; backoff(0) is the delay before the first retry. Returns the result and error from the last attempt.

shouldRetry controls which errors trigger a retry. When non-nil, only errors for which shouldRetry returns true are retried; non-retryable errors are returned immediately without backoff. When nil, all errors are retried.

Context cancellation is checked before each attempt and during backoff waits. Panics if maxAttempts < 1, backoff is nil, or fn is nil.

func Throttle

func Throttle[T, R any](n int, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)

Throttle wraps fn with count-based concurrency control. At most n calls to fn execute concurrently. The returned function blocks until a slot is available, then calls fn. The returned function is safe for concurrent use from multiple goroutines. Panics if n <= 0 or fn is nil.

func ThrottleWeighted

func ThrottleWeighted[T, R any](capacity int, cost func(T) int, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)

ThrottleWeighted wraps fn with cost-based concurrency control. The total cost of concurrently-executing calls never exceeds capacity. The returned function blocks until enough budget is available. The returned function is safe for concurrent use from multiple goroutines.

Token acquisition is serialized to prevent partial-acquire deadlock. This means a high-cost waiter blocks later callers even if capacity is available for them (head-of-line blocking).

Panics if capacity <= 0, cost is nil, or fn is nil. Per-call: panics if cost(t) <= 0 or cost(t) > capacity.

func WithBreaker

func WithBreaker[T, R any](b *Breaker, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)

WithBreaker wraps fn with circuit breaker protection from b. The returned function has the standard hof signature for composition with Retry, Throttle, and other wrappers.

If ctx is already cancelled or expired before admission, the error is returned immediately without affecting breaker state or metrics.

Context cancellation (context.Canceled) does not count as a failure. context.DeadlineExceeded counts as a failure by default (controllable via ShouldCount). Errors where ShouldCount returns false and context.Canceled do not break the consecutive-failure streak; only a success resets it.

If fn panics during a half-open probe, the breaker records a failure and reopens before the panic propagates. Panics during closed-state calls do not affect breaker state.

Panics if b or fn is nil.

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 ConstantBackoff

func ConstantBackoff(delay time.Duration) Backoff

ConstantBackoff returns a Backoff that always waits delay.

func ExponentialBackoff

func ExponentialBackoff(initial time.Duration) Backoff

ExponentialBackoff returns a Backoff with full jitter: random in [0, initial * 2^n). 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 added in v0.56.0

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

Decorator wraps a Func, returning a Func with the same signature.

func CircuitBreaker added in v0.56.0

func CircuitBreaker[T, R any](b *Breaker) Decorator[T, R]

CircuitBreaker returns a Decorator that wraps a Func with circuit breaker protection. See WithBreaker for details.

func ErrMapper added in v0.56.0

func ErrMapper[T, R any](mapper func(error) error) Decorator[T, R]

ErrMapper returns a Decorator that transforms errors. See MapErr for details.

func OnError added in v0.56.0

func OnError[T, R any](handler func(error)) Decorator[T, R]

OnError returns a Decorator that calls handler on error. See OnErr for details.

func Retrier added in v0.56.0

func Retrier[T, R any](maxAttempts int, backoff Backoff, shouldRetry func(error) bool) Decorator[T, R]

Retrier returns a Decorator that retries on error with the given backoff strategy. See Retry for details.

func Throttler added in v0.56.0

func Throttler[T, R any](n int) Decorator[T, R]

Throttler returns a Decorator that bounds concurrent calls. See Throttle for details.

func ThrottlerWeighted added in v0.56.0

func ThrottlerWeighted[T, R any](capacity int, cost func(T) int) Decorator[T, R]

ThrottlerWeighted returns a Decorator that bounds concurrent calls by total cost. See ThrottleWeighted for details.

type Func added in v0.56.0

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

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

func From added in v0.56.0

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

From wraps a plain function as a Func. Go infers the type parameters, so you don't need to specify them explicitly:

call.From(fetchUser).With(...)

func (Func[T, R]) With added in v0.56.0

func (f Func[T, R]) With(ds ...Decorator[T, R]) Func[T, R]

With applies decorators to f in order. Each decorator wraps the result of the previous one (innermost-first):

call.Func[string, User](fetchUser).With(A, B, C)

produces C(B(A(fetchUser))). A is innermost, C is outermost.

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