Arrea.CircuitBreaker (Arrea v2.1.0)

Copy Markdown View Source

Circuit Breaker for system protection.

Implements the Circuit Breaker pattern with three states:

  • :closed — Normal operation; calls are executed directly.
  • :open — Calls are blocked immediately after exceeding the consecutive failures threshold.
  • :half_open — Probe state after the recovery timeout. A single call is allowed to verify whether the system has recovered.

Atomic decision

The decision to allow or block execution is taken atomically inside the GenServer (get_state_and_check), eliminating the race condition between reading the state and executing the function.

Closing from half_open

In :half_open state, max(1, threshold / 2) consecutive successes are required to close the circuit, mitigating the risk of premature closure under concurrent calls.

A failure in :half_open resets the accumulated success counter on the way back to :open, guaranteeing that the next recovery attempt starts from zero.

Timeout measurement

System.monotonic_time/1 is used to compute the interval since the last failure, immune to system clock adjustments (NTP, manual changes, etc.).

Registry

Each breaker is registered through Registry with a unique name under Arrea.CircuitBreaker.Registry.

Summary

Functions

Executes a function protected by the circuit breaker.

Returns a specification to start this module under a supervisor.

Notifies the circuit breaker of an execution failure.

Returns the current state of the circuit breaker (:closed, :open, :half_open).

Starts a circuit breaker with a unique name (required in opts).

Notifies the circuit breaker of a successful execution.

Types

state()

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

Functions

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

@spec call(atom(), (-> term()), keyword()) :: {:ok, term()} | {:error, term()}

Executes a function protected by the circuit breaker.

  • If the circuit is closed or half_open: the function is executed.
  • If the circuit is open and the timeout has not elapsed: returns {:error, :circuit_open} without executing anything.
  • If the breaker is not registered: the function is executed directly (behaviour equivalent to a closed circuit).

Examples

iex> CircuitBreaker.call(:my_breaker, fn -> :ok end)
{:ok, :ok}

iex> CircuitBreaker.call(:my_breaker, fn -> raise "boom" end)
{:error, :execution_failed}

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

failure(name)

@spec failure(atom()) :: :ok

Notifies the circuit breaker of an execution failure.

get_state(name)

@spec get_state(atom()) :: state()

Returns the current state of the circuit breaker (:closed, :open, :half_open).

Returns :closed if the breaker is not registered.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Starts a circuit breaker with a unique name (required in opts).

Options

  • :name — Unique breaker name (required)
  • :id — Alias of :name, accepted for convenience
  • :threshold — Number of consecutive failures to open the circuit (default: 5)
  • :timeout — Time in ms before transitioning to :half_open (default: 60_000)

success(name)

@spec success(atom()) :: :ok

Notifies the circuit breaker of a successful execution.