Documentation ¶
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrCircuitOpen is returned when a circuit is open and not allowing calls through. ErrCircuitOpen = Error{/* contains filtered or unexported fields */} // ErrConcurrencyLimitReached is returned by a [Circuit] using [WithConcurrencyLimit] in non-blocking mode when the // set limit is reached. ErrConcurrencyLimitReached = Error{/* contains filtered or unexported fields */} // ErrWaitingForSlot is returned by a [Circuit] using [WithConcurrencyLimit] in blocking mode when a context error // occurs while waiting for a slot. ErrWaitingForSlot = Error{/* contains filtered or unexported fields */} )
Functions ¶
func IgnoreContextCanceled ¶ added in v0.2.0
IgnoreContextCanceled is a helper function for WithFailureCondition that ignores context.Canceled errors.
Types ¶
type Breaker ¶
type Breaker interface { Option // breakers can also modify or sanity-check their circuit's options // contains filtered or unexported methods }
Breaker is the interface implemented by the different breakers, responsible for actually opening the circuit. Each implementation behaves differently when deciding whether to open the circuit upon failure.
type BreakerMiddleware ¶
type BreakerMiddleware interface {
Wrap(ObserverFactory) (ObserverFactory, error)
}
BreakerMiddleware wraps an ObserverFactory and returns a new ObserverFactory.
func ConcurrencyLimiter ¶
func ConcurrencyLimiter(limit int64, block bool) BreakerMiddleware
ConcurrencyLimiter is a BreakerMiddleware that sets the maximum number of concurrent calls to the provided limit. If the limit is reached, the circuit's behavior depends on the blocking parameter:
- it either returns ErrConcurrencyLimitReached immediately if blocking is false
- or blocks until a slot is available if blocking is true, potentially returning ErrWaitingForSlot. The returned error wraps the underlying cause (e.g. context.Canceled or context.DeadlineExceeded).
Example ¶
package main import ( "context" "fmt" "log" "time" "github.com/exaring/hoglet" ) func main() { h, err := hoglet.NewCircuit( func(ctx context.Context, _ any) (any, error) { select { case <-ctx.Done(): case <-time.After(time.Second): } return nil, nil }, hoglet.NewSlidingWindowBreaker(10, 0.1), hoglet.WithBreakerMiddleware(hoglet.ConcurrencyLimiter(1, false)), ) if err != nil { log.Fatal(err) } errCh := make(chan error) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go func() { // use up the concurrency limit _, _ = h.Call(ctx, 42) }() // ensure call above actually started time.Sleep(time.Millisecond * 100) go func() { defer close(errCh) _, err := h.Call(ctx, 42) if err != nil { errCh <- err } }() for err := range errCh { fmt.Println(err) } }
Output: hoglet: concurrency limit reached
type BreakerMiddlewareFunc ¶ added in v0.2.2
type BreakerMiddlewareFunc func(ObserverFactory) (ObserverFactory, error)
func (BreakerMiddlewareFunc) Wrap ¶ added in v0.2.2
func (f BreakerMiddlewareFunc) Wrap(of ObserverFactory) (ObserverFactory, error)
type Circuit ¶
type Circuit[IN, OUT any] struct { // contains filtered or unexported fields }
Circuit wraps a function and behaves like a simple circuit and breaker: it opens when the wrapped function fails and stops calling the wrapped function until it closes again, returning ErrCircuitOpen in the meantime.
A zero Circuit will panic, analogous to calling a nil function variable. Initialize with NewCircuit.
func NewCircuit ¶
func NewCircuit[IN, OUT any](f WrappedFunc[IN, OUT], breaker Breaker, opts ...Option) (*Circuit[IN, OUT], error)
NewCircuit instantiates a new Circuit that wraps the provided function. See Circuit.Call for calling semantics. A Circuit with a nil breaker is a noop wrapper around the provided function and will never open.
func (*Circuit[IN, OUT]) Call ¶
Call calls the wrapped function if the circuit is closed and returns its result. If the circuit is open, it returns ErrCircuitOpen.
The wrapped function is called synchronously, but possible context errors are recorded as soon as they occur. This ensures the circuit opens quickly, even if the wrapped function blocks.
By default, all errors are considered failures (including context.Canceled), but this can be customized via WithFailureCondition and IgnoreContextCanceled.
Panics are observed as failures, but are not recovered (i.e.: they are "repanicked" instead).
func (*Circuit[IN, OUT]) ObserverForCall ¶
ObserverForCall returns an Observer for the incoming call. It is called exactly once per call to Circuit.Call, before calling the wrapped function. If the breaker is open, it returns ErrCircuitOpen as an error and a nil Observer. If the breaker is closed, it returns a non-nil Observer that will be used to observe the result of the call.
It implements ObserverFactory, so that the Circuit can act as the base for BreakerMiddleware.
type EWMABreaker ¶
type EWMABreaker struct {
// contains filtered or unexported fields
}
EWMABreaker is a Breaker that uses an exponentially weighted moving failure rate. See NewEWMABreaker for details.
A zero EWMABreaker is ready to use, but will never open.
Example ¶
package main import ( "context" "fmt" "log" "time" "github.com/exaring/hoglet" ) type Foo struct { Bar int } func foo(ctx context.Context, bar int) (Foo, error) { if bar > 10 { return Foo{}, fmt.Errorf("bar is too high!") } return Foo{Bar: bar}, nil } func main() { h, err := hoglet.NewCircuit( foo, hoglet.NewEWMABreaker(10, 0.1), hoglet.WithHalfOpenDelay(time.Second), ) if err != nil { log.Fatal(err) } f, err := h.Call(context.Background(), 1) if err != nil { log.Fatal(err) } fmt.Println(f.Bar) _, err = h.Call(context.Background(), 100) fmt.Println(err) _, err = h.Call(context.Background(), 2) fmt.Println(err) time.Sleep(time.Second) // wait for half-open delay f, err = h.Call(context.Background(), 3) if err != nil { log.Fatal(err) } fmt.Println(f.Bar) }
Output: 1 bar is too high! hoglet: breaker is open 3
func NewEWMABreaker ¶
func NewEWMABreaker(sampleCount uint, failureThreshold float64) *EWMABreaker
NewEWMABreaker creates a new EWMABreaker with the given sample count and threshold. It uses an Exponentially Weighted Moving Average to calculate the current failure rate.
⚠️ This is an observation-based breaker, which means it requires new calls to be able to update the failure rate, and therefore REQUIRES the circuit to set a half-open threshold via WithHalfOpenDelay. Otherwise an open circuit will never observe any successes and thus never close.
Compared to the SlidingWindowBreaker, this breaker responds faster to failure bursts, but is more lenient with constant failure rates.
The sample count is used to determine how fast previous observations "decay". A value of 1 causes a single sample to be considered. A higher value slows down convergence. As a rule of thumb, breakers with higher throughput should use higher sample counts to avoid opening up on small hiccups.
The failureThreshold is the failure rate above which the breaker should open (0.0-1.0).
type Error ¶
type Error struct {
// contains filtered or unexported fields
}
Error is the error type used for circuit breaker errors. It can be used to separate circuit errors from errors returned by the wrapped function.
type Observer ¶
type Observer interface { // Observe is called after the wrapped function returns. If [ObserverForCall] returns a non-nil [Observer], it will be // called exactly once. Observe(failure bool) }
Observer is used to observe the result of a single wrapped call through the circuit breaker. Calls in an open circuit cause no observer to be created.
type ObserverFactory ¶
type ObserverFactory interface { // ObserverForCall returns an [Observer] for the incoming call. // It is called with the current [State] of the circuit, before calling the wrapped function. ObserverForCall(context.Context, State) (Observer, error) }
ObserverFactory is an interface that allows customizing the per-call observer creation.
type ObserverFunc ¶
type ObserverFunc func(bool)
ObserverFunc is a helper to turn any function into an Observer.
func (ObserverFunc) Observe ¶
func (o ObserverFunc) Observe(failure bool)
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
func WithBreakerMiddleware ¶
func WithBreakerMiddleware(bm BreakerMiddleware) Option
WithBreakerMiddleware allows wrapping the Breaker via a BreakerMiddleware. Middlewares are processed from innermost to outermost, meaning the first added middleware is the closest to the wrapped function. ⚠️ This means ordering is significant: since "outer" middleware may react differently depending on the output of "inner" middleware. E.g.: the optional prometheus middleware can report metrics about the ConcurrencyLimiter middleware and should therefore be AFTER it in the parameter list.
func WithFailureCondition ¶
WithFailureCondition allows specifying a filter function that determines whether an error should open the breaker. If the provided function returns true, the error is considered a failure and the breaker may open (depending on the breaker logic). The default filter considers all non-nil errors as failures (err != nil).
This does not modify the error returned by Circuit.Call. It only affects the circuit itself.
func WithHalfOpenDelay ¶
WithHalfOpenDelay sets the duration the circuit will stay open before switching to the half-open state, where a limited (~1) amount of calls are allowed that - if successful - may re-close the breaker.
type SlidingWindowBreaker ¶
type SlidingWindowBreaker struct {
// contains filtered or unexported fields
}
SlidingWindowBreaker is a Breaker that uses a sliding window to determine the error rate.
Example ¶
package main import ( "context" "fmt" "log" "time" "github.com/exaring/hoglet" ) type Foo struct { Bar int } func foo(ctx context.Context, bar int) (Foo, error) { if bar > 10 { return Foo{}, fmt.Errorf("bar is too high!") } return Foo{Bar: bar}, nil } func main() { h, err := hoglet.NewCircuit( foo, hoglet.NewSlidingWindowBreaker(time.Second, 0.1), ) if err != nil { log.Fatal(err) } f, err := h.Call(context.Background(), 1) if err != nil { log.Fatal(err) } fmt.Println(f.Bar) _, err = h.Call(context.Background(), 100) fmt.Println(err) _, err = h.Call(context.Background(), 2) fmt.Println(err) time.Sleep(time.Second) // wait for sliding window f, err = h.Call(context.Background(), 3) if err != nil { log.Fatal(err) } fmt.Println(f.Bar) }
Output: 1 bar is too high! hoglet: breaker is open 3
func NewSlidingWindowBreaker ¶
func NewSlidingWindowBreaker(windowSize time.Duration, failureThreshold float64) *SlidingWindowBreaker
NewSlidingWindowBreaker creates a new SlidingWindowBreaker with the given window size and failure rate threshold.
This is a time-based breaker, which means it will revert back to closed after its window size has passed: if no observations are made in the window, the failure rate is effectively zero. This also means: if the circuit has a halfOpenDelay and it is bigger than windowSize, the breaker will never enter half-open state and will directly close instead. Conversely, if halfOpenDelay is smaller than windowSize, the errors observed in the last window will still count proportionally in half-open state, which will lead to faster re-opening on errors.
The windowSize is the time interval over which to calculate the failure rate.
The failureThreshold is the failure rate above which the breaker should open (0.0-1.0).