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 Fn
- func (f Fn[T, R]) Apply(ds ...Decorator[T, R]) Fn[T, R]
- func (f Fn[T, R]) Breaker(b *Breaker) Fn[T, R]
- func (f Fn[T, R]) MapError(mapper func(error) error) Fn[T, R]
- func (f Fn[T, R]) OnError(handler func(error)) Fn[T, R]
- func (f Fn[T, R]) Retry(max int, backoff Backoff, shouldRetry func(error) bool) Fn[T, R]
- type Snapshot
- 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. Use with Fn.Apply for custom decorators.
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
}
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 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
Apply applies custom decorators to f in order (innermost-first).
func (Fn[T, R]) Breaker ¶ added in v0.109.0
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
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
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
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.