Bourse.CircuitBreaker (bourse v0.1.0)

Copy Markdown View Source

Per-exchange circuit breakers using the :fuse Erlang library.

Prevents cascade failures when exchanges are down. Each exchange has isolated state — binance down does not affect bybit.

How It Works

  1. Each registered exchange uses its generated module atom as the fuse name
  2. Fuses are installed lazily on first request
  3. After N failures within M milliseconds, the circuit opens
  4. Opened circuits reject requests immediately (fast fail)
  5. After reset timeout, circuit closes and allows requests again

What Triggers the Circuit

Melt decisions flow from the Phase 13 retry classification carried on a normalized Bourse.Error (the :network and :server_busy buckets), with a raw HTTP 5xx / transport fallback so transport-level failures still trip.

ResponseMelts?Reason
HTTP 500+YesServer error
Timeouts (transport or body :network)YesServer unresponsive
Connection refusedYesServer unavailable
Body-level exchange_not_available (:server_busy)YesExchange down/maintenance
HTTP 429 / :rate_limitNoHandled by rate limiter
HTTP 4xx / :auth / :non_retryableNoClient error, not server issue

Configuration

config :bourse, :circuit_breaker,
  enabled: true,
  max_failures: 5,
  window_ms: 10_000,
  reset_ms: 15_000

Summary

Functions

Returns status of all installed circuit breakers.

Checks if requests are allowed for an exchange.

Returns circuit breaker configuration.

Records a failed request. Enough melts within the window opens the circuit.

Records the result of a request using should_melt?/1 logic.

Records a successful request. Success prevents further melts.

Resets a circuit breaker for an exchange.

Resets a circuit breaker, raising on error.

Determines if a response should trip the circuit breaker.

Returns the status of a circuit breaker for an exchange.

Functions

all_statuses()

@spec all_statuses() :: %{required(String.t()) => :ok | :blown}

Returns status of all installed circuit breakers.

check(exchange_id)

@spec check(String.t()) :: :ok | :blown

Checks if requests are allowed for an exchange.

Installs the fuse lazily if not already installed.

config()

@spec config() :: %{
  enabled: boolean(),
  max_failures: pos_integer(),
  window_ms: pos_integer(),
  reset_ms: pos_integer()
}

Returns circuit breaker configuration.

record_failure(exchange_id)

@spec record_failure(String.t()) :: :ok

Records a failed request. Enough melts within the window opens the circuit.

record_result(exchange_id, result)

@spec record_result(String.t(), term()) :: :ok

Records the result of a request using should_melt?/1 logic.

Accepts either the raw result from Req ({:ok, %Req.Response{}} or {:error, reason}) or the normalized Bourse.Error outcome returned by Bourse.HTTP.

record_success(exchange_id)

@spec record_success(String.t()) :: :ok

Records a successful request. Success prevents further melts.

reset(exchange_id)

@spec reset(String.t()) :: :ok | {:error, :not_found}

Resets a circuit breaker for an exchange.

reset!(exchange_id)

@spec reset!(String.t()) :: :ok

Resets a circuit breaker, raising on error.

should_melt?(arg1)

@spec should_melt?(term()) :: boolean()

Determines if a response should trip the circuit breaker.

Accepts both raw Req results and normalized {:error, %Bourse.Error{}} outcomes. For a normalized error the decision flows from the Phase 13 retry classification: :network and :server_busy melt, everything else (:rate_limit, :auth, :non_retryable, unclassified) does not — with a raw HTTP 5xx fallback so server errors melt regardless of body classification.

Melts on: HTTP 500+, transport errors, :network/:server_busy errors. Does NOT melt on: HTTP 429, HTTP 4xx, :auth/:non_retryable errors, successes.

status(exchange_id)

@spec status(String.t()) :: :ok | :blown | :not_installed

Returns the status of a circuit breaker for an exchange.

  • :ok — circuit closed, requests allowed
  • :blown — circuit open, requests rejected
  • :not_installed — no fuse yet (no requests made)