hoglet

package module
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 14, 2024 License: MIT Imports: 8 Imported by: 1

README

Build Status Go Reference Go Report Card

hoglet

Simple low-overhead circuit breaker library.

Usage

h, err := hoglet.NewCircuit(
    func(ctx context.Context, bar int) (Foo, error) {
        if bar == 42 {
            return Foo{Bar: bar}, nil
        }
        return Foo{}, fmt.Errorf("bar is not 42")
    },
    hoglet.NewSlidingWindowBreaker(5*time.Second, 0.1),
    hoglet.WithFailureCondition(hoglet.IgnoreContextCanceled),
)
/* if err != nil ... */

f, _ := h.Call(context.Background(), 42)
fmt.Println(f.Bar) // 42

_, err = h.Call(context.Background(), 0)
fmt.Println(err) // bar is not 42

_, err = h.Call(context.Background(), 42)
fmt.Println(err) // hoglet: breaker is open

time.Sleep(5 * time.Second)

f, _ = h.Call(context.Background(), 42)
fmt.Println(f.Bar) // 42

Operation

Each call to the wrapped function (via Circuit.Call) is tracked and its result "observed". Breakers then react to these observations according to their own logic, optionally opening the circuit.

An open circuit does not allow any calls to go through, and will return an error immediately.

If the wrapped function blocks, Circuit.Call will block as well, but any context cancellations or expirations will count towards the failure rate, allowing the circuit to respond timely to failures, while still having well-defined and non-racy behavior around the failed function.

Design

Hoglet prefers throughput to correctness (e.g. by avoiding locks), which means it cannot guarantee an exact number of calls will go through.

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
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

func IgnoreContextCanceled(err error) bool

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:

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

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

func (c *Circuit[IN, OUT]) Call(ctx context.Context, in IN) (out OUT, err error)

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

func (c *Circuit[IN, OUT]) ObserverForCall(_ context.Context, state State) (Observer, error)

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.

func (*Circuit[IN, OUT]) State

func (c *Circuit[IN, OUT]) State() State

State reports the current State of the circuit. It should only be used for informational purposes. To minimize race conditions, the circuit should be called directly instead of checking its state first.

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.

func (Error) Error

func (b Error) Error() string

Error implements the error interface.

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

WithMiddleware 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

func WithFailureCondition(condition func(error) bool) Option

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

func WithHalfOpenDelay(delay time.Duration) Option

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).

type State

type State int

State represents the state of a circuit.

const (
	// StateClosed means a circuit is ready to accept calls.
	StateClosed State = iota
	// StateHalfOpen means a limited (~1) number of calls is allowed through.
	StateHalfOpen
	// StateOpen means a circuit is not accepting calls.
	StateOpen
)

func (State) String

func (s State) String() string

type WrappedFunc

type WrappedFunc[IN, OUT any] func(context.Context, IN) (OUT, error)

WrappedFunc is the type of the function wrapped by a Breaker.

Directories

Path Synopsis
extensions
prometheus Module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL