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 ¶
- Variables
- func ConsecutiveFailures(n int) func(Snapshot) bool
- type Backoff
- type Breaker
- type BreakerConfig
- type BreakerState
- type Decorator
- type Features
- type Fn
- func (f Fn[T, R]) With(feat Features) Fn[T, R]
- func (f Fn[T, R]) WithBreaker(b *Breaker) Fn[T, R]
- func (f Fn[T, R]) WithMapError(mapper func(error) error) Fn[T, R]
- func (f Fn[T, R]) WithOnError(handler func(error)) Fn[T, R]
- func (f Fn[T, R]) WithRetry(maxAttempts int, backoff Backoff, shouldRetry func(error) bool) Fn[T, R]
- func (f Fn[T, R]) WithThrottle(n int) Fn[T, R]
- func (f Fn[T, R]) WithThrottleWeighted(capacity int, cost func(T) int) Fn[T, R]
- type RetryConfig
- type Snapshot
- type ThrottleConfig
- type Transition
Examples ¶
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.
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 ExpBackoff ¶
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.
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 ¶
Decorator wraps an Fn, returning an Fn with the same signature. For custom decorators not covered by Features, apply manually:
decorated := wrap.Fn[T, R](myDecorator(fn))
type Features ¶ added in v0.108.1
type Features struct {
Breaker *Breaker
MapError func(error) error
OnError func(error)
Retry *RetryConfig
Throttle *ThrottleConfig
}
Features configures which decorators to apply. Nil fields are skipped.
Decorators are applied in a fixed order (innermost to outermost): OnError → MapError → Retry → Breaker → Throttle.
type Fn ¶
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
}
// Compose: retry transient errors, then limit concurrency.
resilient := wrap.Func(fetchData).
WithRetry(3, wrap.ExpBackoff(time.Millisecond), nil).
WithThrottle(10)
got, _ := resilient(context.Background(), "abc")
fmt.Println(got)
}
Output: data(abc)
func Func ¶
Func wraps a plain function as an Fn for fluent decoration. Go infers the type parameters from fn:
wrap.Func(fetchUser).With(wrap.Features{
Retry: wrap.Retry(3, wrap.ExpBackoff(time.Second), nil),
})
func (Fn[T, R]) With ¶
With applies the features to f in a fixed order. Nil fields are skipped. Innermost to outermost: OnError → MapError → Retry → Breaker → Throttle.
Example ¶
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
}
// Configure all features in one struct. Library controls decorator order.
resilient := wrap.Func(fetchData).With(wrap.Features{
Retry: wrap.Retry(3, wrap.ExpBackoff(time.Millisecond), nil),
Throttle: wrap.Throttle(10),
})
got, _ := resilient(context.Background(), "abc")
fmt.Println(got)
}
Output: data(abc)
func (Fn[T, R]) WithBreaker ¶
WithBreaker is shorthand for With(Features{Breaker: b}).
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 }
// Protect a dependency with a circuit breaker.
breaker := wrap.NewBreaker(wrap.BreakerConfig{
ResetTimeout: 10 * time.Second,
})
protected := wrap.Func(double).WithBreaker(breaker)
got, _ := protected(context.Background(), 21)
fmt.Println(got)
}
Output: 42
func (Fn[T, R]) WithMapError ¶
WithMapError is shorthand for With(Features{MapError: 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).WithMapError(annotate)
_, err := wrapped(context.Background(), 42)
fmt.Println(err)
}
Output: fetchUser(42): not found
func (Fn[T, R]) WithOnError ¶
WithOnError is shorthand for With(Features{OnError: handler}).
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).WithOnError(logError)
_, err := observed(context.Background(), 1)
fmt.Printf("returned: %v\n", err)
}
Output: logged: not found returned: not found
func (Fn[T, R]) WithRetry ¶
func (f Fn[T, R]) WithRetry(maxAttempts int, backoff Backoff, shouldRetry func(error) bool) Fn[T, R]
WithRetry is shorthand for With(Features{Retry: Retry(...)}).
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 }
// Retry up to 3 times with exponential backoff.
resilient := wrap.Func(double).WithRetry(3, wrap.ExpBackoff(time.Millisecond), nil)
got, _ := resilient(context.Background(), 5)
fmt.Println(got)
}
Output: 10
func (Fn[T, R]) WithThrottle ¶
WithThrottle is shorthand for With(Features{Throttle: Throttle(n)}).
Example ¶
package main
import (
"context"
"fmt"
"github.com/binaryphile/fluentfp/wrap"
)
func main() {
// double doubles the input.
double := func(_ context.Context, n int) (int, error) { return n * 2, nil }
// Allow at most 5 concurrent calls.
limited := wrap.Func(double).WithThrottle(5)
got, _ := limited(context.Background(), 3)
fmt.Println(got)
}
Output: 6
type RetryConfig ¶ added in v0.108.1
RetryConfig configures the retry feature.
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 ThrottleConfig ¶ added in v0.108.1
type ThrottleConfig struct {
N int
}
ThrottleConfig configures count-based concurrency control.
func Throttle ¶ added in v0.108.1
func Throttle(n int) *ThrottleConfig
Throttle returns a ThrottleConfig for use in Features.
type Transition ¶
type Transition struct {
From BreakerState
To BreakerState
At time.Time
}
Transition describes a circuit breaker state change.