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 ¶
- Variables
- func ConsecutiveFailures(n int) func(Snapshot) bool
- func MapErr[T, R any](fn func(context.Context, T) (R, error), mapper func(error) error) func(context.Context, T) (R, error)
- func OnErr[T, R any](fn func(context.Context, T) (R, error), onErr func(error)) func(context.Context, T) (R, error)
- func Retry[T, R any](maxAttempts int, backoff Backoff, shouldRetry func(error) bool, ...) func(context.Context, T) (R, error)
- func Throttle[T, R any](n int, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)
- func ThrottleWeighted[T, R any](capacity int, cost func(T) int, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)
- func WithBreaker[T, R any](b *Breaker, fn func(context.Context, T) (R, error)) func(context.Context, T) (R, error)
- type Backoff
- type Breaker
- type BreakerConfig
- type BreakerState
- type Snapshot
- type Transition
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
Backoff computes the delay before retry number n (0-indexed). Called between attempts: backoff(0) is the delay before the first retry.
func ConstantBackoff ¶
ConstantBackoff returns a Backoff that always waits delay.
func ExponentialBackoff ¶
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.
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 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.