HTTPower.Middleware.CircuitBreaker (HTTPower v0.22.0)

Copy Markdown 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

# When circuit is closed (healthy), executes the function and returns its result:
iex> HTTPower.CircuitBreaker.call("api.example.com", fn ->
...>   HTTPower.get("https://api.example.com/users")
...> end)
{:ok, %HTTPower.Response{status: 200, body: ...}}

# When circuit is open (too many failures), short-circuits immediately:
iex> HTTPower.CircuitBreaker.call("api.example.com", fn ->
...>   HTTPower.get("https://api.example.com/users")
...> end)
{:error, %HTTPower.Error{reason: :service_unavailable}}

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.