# `Tinkex.CircuitBreaker`
[🔗](https://github.com/North-Shore-AI/tinkex/blob/v0.4.0/lib/tinkex/circuit_breaker.ex#L1)

Per-endpoint circuit breaker for resilient API calls.

Implements the circuit breaker pattern to prevent cascading failures
when an endpoint is experiencing issues. The circuit has three states:

- **Closed**: Normal operation. Requests flow through, failures are counted.
- **Open**: Requests are rejected immediately. After a timeout, transitions to half-open.
- **Half-Open**: Limited requests allowed to test if the endpoint has recovered.

## Configuration

- `failure_threshold`: Number of failures before opening circuit (default: 5)
- `reset_timeout_ms`: Time in open state before trying half-open (default: 30,000ms)
- `half_open_max_calls`: Calls allowed in half-open state (default: 1)

## Usage

    cb = CircuitBreaker.new("sampling-endpoint", failure_threshold: 3)

    {result, cb} = CircuitBreaker.call(cb, fn ->
      # Make API call
      {:ok, response}
    end)

## ETS-based Registry

For multi-process scenarios, use `CircuitBreaker.Registry` to store
circuit breaker state in ETS:

    CircuitBreaker.Registry.call("endpoint-name", fn ->
      # Make API call
    end)

# `state`

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

# `t`

```elixir
@type t() :: %Tinkex.CircuitBreaker{
  failure_count: non_neg_integer(),
  failure_threshold: pos_integer(),
  half_open_calls: non_neg_integer(),
  half_open_max_calls: pos_integer(),
  name: String.t(),
  opened_at: integer() | nil,
  reset_timeout_ms: pos_integer(),
  state: state()
}
```

# `allow_request?`

```elixir
@spec allow_request?(t()) :: boolean()
```

Check if a request should be allowed.

Returns `true` if the circuit is closed or half-open (and under limit).
Returns `false` if the circuit is open.

# `call`

```elixir
@spec call(t(), (-&gt; result), keyword()) :: {result | {:error, :circuit_open}, t()}
when result: term()
```

Execute a function through the circuit breaker.

Returns `{result, updated_circuit_breaker}`.

If the circuit is open, returns `{:error, :circuit_open}` without
executing the function.

## Options

- `:success?` - Custom function to determine if result is a success.
  Default: `{:ok, _}` is success, `{:error, _}` is failure.

## Examples

    {result, cb} = CircuitBreaker.call(cb, fn ->
      Tinkex.API.Sampling.sample_async(request, opts)
    end)

    # Custom success classification (4xx errors don't trip breaker)
    {result, cb} = CircuitBreaker.call(cb, fn ->
      Tinkex.API.post("/endpoint", body, opts)
    end, success?: fn
      {:ok, _} -> true
      {:error, %{status: status}} when status < 500 -> true
      _ -> false
    end)

# `new`

```elixir
@spec new(
  String.t(),
  keyword()
) :: t()
```

Create a new circuit breaker.

## Options

- `:failure_threshold` - Failures before opening (default: 5)
- `:reset_timeout_ms` - Open duration before half-open (default: 30,000)
- `:half_open_max_calls` - Calls allowed in half-open (default: 1)

# `record_failure`

```elixir
@spec record_failure(t()) :: t()
```

Record a failed call.

Increments failure count. Opens circuit if threshold reached.

# `record_success`

```elixir
@spec record_success(t()) :: t()
```

Record a successful call.

Resets failure count. Transitions half-open to closed.

# `reset`

```elixir
@spec reset(t()) :: t()
```

Reset the circuit breaker to closed state.

# `state`

```elixir
@spec state(t()) :: state()
```

Get the current state of the circuit breaker.

Accounts for reset timeout transitions from open to half-open.

---

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