HTTPower.Middleware.CircuitBreaker (HTTPower v0.16.0)

View Source

Circuit breaker implementation for HTTPower.

Implements the circuit breaker pattern to protect against cascading failures when calling failing services. The circuit breaker has three states:

  • Closed (normal): Requests pass through, failures are tracked
  • Open (failing): Requests fail immediately without calling the service
  • Half-Open (testing): Limited requests allowed to test recovery

How It Works

  1. Closed State: Requests pass through normally. The circuit breaker tracks failures in a sliding window. If failures exceed the threshold, it transitions to Open.

  2. Open State: All requests fail immediately with :service_unavailable. After a timeout period, the circuit transitions to Half-Open.

  3. Half-Open State: A limited number of test requests are allowed through. If they succeed, the circuit transitions back to Closed. If they fail, the circuit transitions back to Open.

Configuration

config :httpower, :circuit_breaker,
  enabled: true,                    # Enable/disable (default: false)
  failure_threshold: 5,             # Open after N failures
  failure_threshold_percentage: 50, # Or open after N% failure rate
  window_size: 10,                  # Track last N requests
  timeout: 60_000,                  # Stay open for 60s (milliseconds)
  half_open_requests: 1             # Allow N test requests

Usage

# Global circuit breaker
config :httpower, :circuit_breaker,
  enabled: true,
  failure_threshold: 5,
  timeout: 60_000

# Per-client circuit breaker
client = HTTPower.new(
  base_url: "https://api.example.com",
  circuit_breaker: [
    failure_threshold: 3,
    timeout: 30_000
  ]
)

# Per-request circuit breaker key
HTTPower.get(url, circuit_breaker_key: "payment_api")

Example

# After 5 failures, circuit opens
for _ <- 1..5 do
  {:error, _} = HTTPower.get("https://failing-api.com/endpoint")
end

# Subsequent requests fail immediately
{:error, %{reason: :service_unavailable}} =
  HTTPower.get("https://failing-api.com/endpoint")

# After 60 seconds, circuit enters half-open
# Next successful request closes the circuit
:timer.sleep(60_000)
{:ok, _} = HTTPower.get("https://failing-api.com/endpoint")

Summary

Functions

Checks if a request should be allowed through the circuit breaker.

Returns a specification to start this module under a supervisor.

Manually closes a circuit.

Gets the current state of a circuit.

Feature callback for the HTTPower pipeline.

Manually opens a circuit.

Records a failed request for the circuit.

Records a successful request for the circuit.

Resets a circuit to its initial closed state.

Starts the circuit breaker GenServer.

Types

circuit_breaker_config()

@type circuit_breaker_config() :: [
  enabled: boolean(),
  failure_threshold: pos_integer(),
  failure_threshold_percentage: pos_integer(),
  window_size: pos_integer(),
  timeout: pos_integer(),
  half_open_requests: pos_integer()
]

circuit_key()

@type circuit_key() :: String.t()

circuit_state()

@type circuit_state() :: %{
  state: state(),
  requests: [request_result()],
  opened_at: integer() | nil,
  half_open_attempts: integer()
}

request_result()

@type request_result() :: {:success | :failure, integer()}

state()

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

Functions

call(circuit_key, fun, config \\ [])

@spec call(
  circuit_key(),
  (-> {:ok, term()} | {:error, term()}),
  circuit_breaker_config()
) ::
  {:ok, term()} | {:error, term()}

Checks if a request should be allowed through the circuit breaker.

Returns:

  • {:ok, :allowed} if request can proceed
  • {:error, :service_unavailable} if circuit is open
  • {:ok, :disabled} if circuit breaker is disabled

Examples

iex> HTTPower.CircuitBreaker.call("api.example.com", fn ->
...>   HTTPower.get("https://api.example.com/users")
...> end)
{:ok, response}

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

close_circuit(circuit_key)

@spec close_circuit(circuit_key()) :: :ok

Manually closes a circuit.

Useful for testing or manual intervention.

get_state(circuit_key)

@spec get_state(circuit_key()) :: state() | nil

Gets the current state of a circuit.

Returns :closed, :open, :half_open, or nil if circuit doesn't exist.

handle_request(request, config)

Feature callback for the HTTPower pipeline.

Checks circuit breaker state and stores info for post-request recording.

Returns:

  • :ok if circuit is closed (continue with request)
  • {:ok, request} with circuit breaker info stored in private
  • {:error, reason} if circuit is open (fail immediately)

Examples

iex> request = %HTTPower.Request{url: "https://api.example.com", ...}
iex> HTTPower.CircuitBreaker.handle_request(request, [failure_threshold: 5])
{:ok, modified_request}

open_circuit(circuit_key)

@spec open_circuit(circuit_key()) :: :ok

Manually opens a circuit.

Useful for testing or manual intervention.

record_failure(circuit_key, config \\ [])

@spec record_failure(circuit_key(), circuit_breaker_config()) :: :ok

Records a failed request for the circuit.

record_success(circuit_key, config \\ [])

@spec record_success(circuit_key(), circuit_breaker_config()) :: :ok

Records a successful request for the circuit.

reset_circuit(circuit_key)

@spec reset_circuit(circuit_key()) :: :ok

Resets a circuit to its initial closed state.

start_link(opts \\ [])

Starts the circuit breaker GenServer.