# `Bourse.CircuitBreaker`
[🔗](https://github.com/ZenHive/bourse/blob/main/lib/bourse/circuit_breaker.ex#L1)

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.

| Response | Melts? | Reason |
|----------|--------|--------|
| HTTP 500+ | Yes | Server error |
| Timeouts (transport or body `:network`) | Yes | Server unresponsive |
| Connection refused | Yes | Server unavailable |
| Body-level `exchange_not_available` (`:server_busy`) | Yes | Exchange down/maintenance |
| HTTP 429 / `:rate_limit` | **No** | Handled by rate limiter |
| HTTP 4xx / `:auth` / `:non_retryable` | **No** | Client error, not server issue |
| `:invalid_nonce` (`:network` class) | **No** | Client clock/nonce drift — retryable for the caller, never venue downtime |

## Configuration

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

# `all_statuses`

```elixir
@spec all_statuses() :: %{required(String.t()) =&gt; :ok | :blown}
```

Returns status of all installed circuit breakers.

# `check`

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

Checks if requests are allowed for an exchange.

Installs the fuse lazily if not already installed.

# `config`

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

Returns circuit breaker configuration.

# `record_failure`

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

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

# `record_result`

```elixir
@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`

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

Records a successful request. Success prevents further melts.

# `reset`

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

Resets a circuit breaker for an exchange.

# `reset!`

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

Resets a circuit breaker, raising on error.

# `should_melt?`

```elixir
@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`

```elixir
@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)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
