Documentation
¶
Overview ¶
Package resilience provides composable fault-tolerance primitives for Go services — a circuit breaker, retry with backoff, timeout, bulkhead (concurrency limiter), and a token-bucket rate limiter, modelled on gobreaker / resilience4j.
Every primitive shares the same shape so they compose cleanly:
type Operation func(ctx context.Context) (any, error) type Middleware func(Operation) Operation
A primitive exposes Execute(ctx, fn) for runtime use and a Middleware() method for static composition. Wrap chains middlewares outermost-first:
guard := resilience.Wrap(
breaker.Middleware(), // outermost: sees retry's final result
retry.Middleware(), // re-runs the timeout-wrapped call
timeout.Middleware(), // innermost: bounds each attempt
)
out, err := guard(fn)(ctx)
The generic Do[T] helpers recover the concrete return type without the caller writing a type assertion.
All primitives are goroutine-safe and honour context cancellation. Time is read through an injectable Clock so timing behaviour can be tested deterministically (see NewFakeClock).
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrOpenCircuit = errors.New("resilience: circuit breaker is open")
ErrOpenCircuit is returned by the breaker when the circuit is open (or the half-open trial quota is exhausted) and the call is short-circuited.
Functions ¶
func Do ¶
func Do[T any](ctx context.Context, mw Middleware, fn func(ctx context.Context) (T, error)) (T, error)
Do adapts a typed function into an Operation, runs it through mw, and returns the result reasserted to T. A nil mw runs fn directly. If the underlying Operation returns a value that is not a T (only possible when an outer layer substitutes a fallback), the zero value of T is returned alongside any error.
Types ¶
type Breaker ¶
type Breaker struct {
// contains filtered or unexported fields
}
Breaker is a goroutine-safe circuit breaker.
Example ¶
ExampleBreaker drives a circuit breaker through its full lifecycle using an injected FakeClock so the cooldown is deterministic: consecutive failures trip it OPEN, further calls are rejected fast with ErrOpenCircuit, advancing the clock past the cooldown promotes it to HALF-OPEN, and a trial success CLOSES it again.
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/devituz/lagodev/resilience"
)
func main() {
clock := resilience.NewFakeClock(time.Unix(0, 0))
br := resilience.NewBreaker(resilience.BreakerConfig{
Name: "payments",
ConsecutiveFailures: 2, // trip after 2 failures in a row
OpenTimeout: 5 * time.Second, // cooldown before half-open
Clock: clock,
OnStateChange: func(name string, from, to resilience.State) {
fmt.Printf("%s -> %s\n", from, to)
},
})
ctx := context.Background()
boom := errors.New("dependency down")
fail := func(context.Context) (any, error) { return nil, boom }
ok := func(context.Context) (any, error) { return "ok", nil }
// Two consecutive failures trip the breaker open.
_, _ = br.Execute(ctx, fail)
_, _ = br.Execute(ctx, fail)
fmt.Println("state:", br.State())
// While open, calls are short-circuited without invoking fn.
_, err := br.Execute(ctx, ok)
fmt.Println("rejected fast:", errors.Is(err, resilience.ErrOpenCircuit))
// Advance past the cooldown: the next observation promotes to half-open.
clock.Advance(5 * time.Second)
fmt.Println("state:", br.State())
// A successful trial call closes the breaker again.
if _, err := br.Execute(ctx, ok); err != nil {
fmt.Println("trial:", err)
}
fmt.Println("state:", br.State())
}
Output: closed -> open state: open rejected fast: true open -> half-open state: half-open half-open -> closed state: closed
func NewBreaker ¶
func NewBreaker(cfg BreakerConfig) *Breaker
NewBreaker builds a Breaker from cfg, filling in defaults.
func (*Breaker) Execute ¶
func (b *Breaker) Execute(ctx context.Context, fn func(ctx context.Context) (any, error)) (any, error)
Execute runs fn under the breaker. It returns ErrOpenCircuit without invoking fn when the circuit is open or the half-open quota is exhausted.
func (*Breaker) Middleware ¶
func (b *Breaker) Middleware() Middleware
Middleware adapts the breaker for use with Wrap.
type BreakerConfig ¶
type BreakerConfig struct {
// Name is included in callbacks for observability.
Name string
// MaxRequests is the number of trial calls allowed in half-open before
// the breaker decides. Defaults to 1.
MaxRequests uint32
// OpenTimeout is how long the breaker stays open before moving to
// half-open. Defaults to 60s.
OpenTimeout time.Duration
// MinRequests is the minimum number of requests in a generation before
// the failure-ratio threshold is considered. Defaults to 0 (always
// considered).
MinRequests uint32
// FailureRatio trips the breaker when the failure ratio meets or
// exceeds this value (and MinRequests is satisfied). 0 disables the
// ratio rule.
FailureRatio float64
// ConsecutiveFailures trips the breaker when this many calls fail in a
// row. 0 disables the consecutive rule. If both rules are disabled the
// default ConsecutiveFailures of 5 is applied.
ConsecutiveFailures uint32
// IsSuccessful classifies an error as a success (true) or failure
// (false). Defaults to "nil error == success". Use it to treat, e.g.,
// context.Canceled or 4xx as non-failures.
IsSuccessful func(err error) bool
// OnStateChange is called on every state transition.
OnStateChange func(name string, from, to State)
// Clock is the time source; defaults to SystemClock.
Clock Clock
}
BreakerConfig configures a Breaker. The zero value is usable; New applies sensible defaults for any unset field.
type Clock ¶
type Clock interface {
// Now returns the current time.
Now() time.Time
// After returns a channel that receives once d has elapsed.
After(d time.Duration) <-chan time.Time
// Sleep blocks for d (or until the channel from After would fire).
Sleep(d time.Duration)
}
Clock abstracts the parts of the time package the primitives depend on so timing behaviour can be driven deterministically in tests. The production implementation (realClock) delegates to the standard library.
var SystemClock Clock = realClock{}
SystemClock is the production Clock used when no clock is injected.
type Counts ¶
type Counts struct {
Requests uint32
TotalSuccesses uint32
TotalFailures uint32
ConsecutiveSuccess uint32
ConsecutiveFailures uint32
}
Counts holds the breaker's rolling tally for the current generation. A generation resets on every state transition.
func (Counts) FailureRatio ¶
FailureRatio returns failures/requests for the current generation, or 0 when there have been no requests.
type FakeClock ¶
type FakeClock struct {
// contains filtered or unexported fields
}
FakeClock is a manually advanced Clock for deterministic tests. Time only moves when Advance is called; timers registered via After fire when the virtual clock passes their deadline. It is goroutine-safe.
func NewFakeClock ¶
NewFakeClock returns a FakeClock anchored at start.
func (*FakeClock) Advance ¶
Advance moves the virtual clock forward by d and fires every timer whose deadline has passed.
func (*FakeClock) BlockedSleepers ¶
BlockedSleepers returns how many timers are currently pending. Tests use it to wait until a goroutine has parked on After/Sleep before advancing.
type Middleware ¶
Middleware decorates an Operation with one resilience concern. Compose several with Wrap.
func Wrap ¶
func Wrap(mws ...Middleware) Middleware
Wrap composes middlewares into a single Middleware. The first argument is the outermost layer: Wrap(a, b, c)(op) == a(b(c(op))). Calling Wrap with no arguments yields an identity middleware.
type Operation ¶
Operation is the unit of work guarded by the resilience primitives. It returns an arbitrary result plus an error; primitives decide success or failure from the error (and, for the breaker, a configurable predicate).
type State ¶
type State int
State is the circuit breaker's operating state.
const ( // StateClosed lets calls through and records outcomes. StateClosed State = iota // StateOpen rejects calls immediately with ErrOpenCircuit until the // open timeout elapses. StateOpen // StateHalfOpen permits a limited number of trial calls to probe // whether the dependency has recovered. StateHalfOpen )