Tinkex.CircuitBreaker (Tinkex v0.3.4)

View Source

Per-endpoint circuit breaker for resilient API calls.

Implements the circuit breaker pattern to prevent cascading failures when an endpoint is experiencing issues. The circuit has three states:

  • Closed: Normal operation. Requests flow through, failures are counted.
  • Open: Requests are rejected immediately. After a timeout, transitions to half-open.
  • Half-Open: Limited requests allowed to test if the endpoint has recovered.

Configuration

  • failure_threshold: Number of failures before opening circuit (default: 5)
  • reset_timeout_ms: Time in open state before trying half-open (default: 30,000ms)
  • half_open_max_calls: Calls allowed in half-open state (default: 1)

Usage

cb = CircuitBreaker.new("sampling-endpoint", failure_threshold: 3)

{result, cb} = CircuitBreaker.call(cb, fn ->
  # Make API call
  {:ok, response}
end)

ETS-based Registry

For multi-process scenarios, use CircuitBreaker.Registry to store circuit breaker state in ETS:

CircuitBreaker.Registry.call("endpoint-name", fn ->
  # Make API call
end)

Summary

Functions

Check if a request should be allowed.

Execute a function through the circuit breaker.

Create a new circuit breaker.

Record a failed call.

Record a successful call.

Reset the circuit breaker to closed state.

Get the current state of the circuit breaker.

Types

state()

@type state() :: :closed | :open | :half_open

t()

@type t() :: %Tinkex.CircuitBreaker{
  failure_count: non_neg_integer(),
  failure_threshold: pos_integer(),
  half_open_calls: non_neg_integer(),
  half_open_max_calls: pos_integer(),
  name: String.t(),
  opened_at: integer() | nil,
  reset_timeout_ms: pos_integer(),
  state: state()
}

Functions

allow_request?(cb)

@spec allow_request?(t()) :: boolean()

Check if a request should be allowed.

Returns true if the circuit is closed or half-open (and under limit). Returns false if the circuit is open.

call(cb, fun, opts \\ [])

@spec call(t(), (-> result), keyword()) :: {result | {:error, :circuit_open}, t()}
when result: term()

Execute a function through the circuit breaker.

Returns {result, updated_circuit_breaker}.

If the circuit is open, returns {:error, :circuit_open} without executing the function.

Options

  • :success? - Custom function to determine if result is a success. Default: {:ok, _} is success, {:error, _} is failure.

Examples

{result, cb} = CircuitBreaker.call(cb, fn ->
  Tinkex.API.Sampling.sample_async(request, opts)
end)

# Custom success classification (4xx errors don't trip breaker)
{result, cb} = CircuitBreaker.call(cb, fn ->
  Tinkex.API.post("/endpoint", body, opts)
end, success?: fn
  {:ok, _} -> true
  {:error, %{status: status}} when status < 500 -> true
  _ -> false
end)

new(name, opts \\ [])

@spec new(
  String.t(),
  keyword()
) :: t()

Create a new circuit breaker.

Options

  • :failure_threshold - Failures before opening (default: 5)
  • :reset_timeout_ms - Open duration before half-open (default: 30,000)
  • :half_open_max_calls - Calls allowed in half-open (default: 1)

record_failure(cb)

@spec record_failure(t()) :: t()

Record a failed call.

Increments failure count. Opens circuit if threshold reached.

record_success(cb)

@spec record_success(t()) :: t()

Record a successful call.

Resets failure count. Transitions half-open to closed.

reset(cb)

@spec reset(t()) :: t()

Reset the circuit breaker to closed state.

state(circuit_breaker)

@spec state(t()) :: state()

Get the current state of the circuit breaker.

Accounts for reset timeout transitions from open to half-open.