# `ExternalService.RateLimiter`
[🔗](https://github.com/jvoegele/external_service/blob/main/lib/external_service/rate_limiter.ex#L1)

The behaviour implemented by rate limiter backends.

A service's limiter is chosen with the `:backend` rate limit option, and
defaults to `ExternalService.RateLimiter.Local`. `ExternalService.RateLimiter.Hammer`
meters against a [Hammer](https://hexdocs.pm/hammer) module, which is the
supported route to a limit shared across a cluster.

## Writing a backend

A backend answers one question, in two forms: may a call proceed right now,
and if not, how long until it may? `c:check/2` answers it and consumes the
call; `c:peek/2` answers it and consumes nothing. Everything else — sleeping,
honoring the `:wait` budget, telemetry, logging — is handled for you, so that
every backend behaves consistently.

    defmodule MyApp.RateLimiter do
      @behaviour ExternalService.RateLimiter

      @impl true
      def init(service, options) do
        {:ok, %{key: service, limit: options[:limit], window: options[:per]}}
      end

      @impl true
      def check(_service, config) do
        case MyStore.increment(config.key, config.window, config.limit) do
          {:ok, _count} -> :ok
          {:throttled, milliseconds} -> {:wait, milliseconds}
        end
      end
    end

Then point a service at it:

    use ExternalService,
      rate_limit: [limit: 100, per: 1_000, backend: {MyApp.RateLimiter, some: :option}]

Backends are **stateless modules**. `c:init/2` returns an opaque config term
that is stored with the rest of the service state and handed back to every
other callback, so a backend needs no process, supervisor, or registry of its
own. Anything mutable it needs — an `:atomics` reference, a connection pool
name, a remote key — travels in that term.

Report a real time-to-next-window from `c:check/2` where you can. Callers sleep
for exactly as long as you say, so an accurate answer paces calls precisely and
makes the `:wait` budget meaningful.

## Driving a limiter directly

Most of the time the limiter is driven for you: `ExternalService.call/3` checks
it before running your function. The functions in this module are for the cases
that fall outside a guarded call.

`peek/1` asks whether a call would be admitted **without consuming anything**,
which is what makes it safe to call speculatively:

    case ExternalService.RateLimiter.peek(:payments) do
      :ok -> start_expensive_work()
      {:wait, ms} -> {:error, {:busy, ms}}
    end

`ExternalService.rate_limited?/1` is the boolean form, symmetric with
`ExternalService.available?/1`.

`request/1` is the write side: it consumes one call's worth of the budget
without running anything, for traffic that reaches the service by some path
other than `call/3`.

    # A batch endpoint that costs three calls against the quota.
    Enum.each(1..3, fn _ -> ExternalService.RateLimiter.request(:payments) end)

Note that `request/1` blocks according to the service's `:wait` setting, just
as a guarded call would.

# `config`

```elixir
@type config() :: term()
```

Backend-private state, produced by `c:init/2` and passed to every other callback.

# `rate_limited`

```elixir
@type rate_limited() ::
  {ExternalService.RateLimiter, :rate_limited, non_neg_integer()}
```

Returned by `call/2` when the wait budget was exhausted before the call could
be admitted, carrying the milliseconds still remaining.

# `service`

```elixir
@type service() :: ExternalService.service()
```

# `t`

```elixir
@type t() ::
  %ExternalService.RateLimiter{
    backend: term(),
    config: term(),
    service: term(),
    sleep: term(),
    wait: term()
  }
  | nil
```

A configured rate limiter.

`nil` means the service is not rate limited, in which case calls pass straight
through.

# `wait`

```elixir
@type wait() :: :infinity | false | non_neg_integer() | nil
```

How long a throttled call may wait before giving up.

`:infinity` waits as long as the limiter requires, `false` never waits, and an
integer is a millisecond budget for the whole call.

A service that does not set `:wait` gets one derived from its `:per` — see
`default_wait/1`. `nil` never survives `new/3`.

# `check`

```elixir
@callback check(service(), config()) :: :ok | {:wait, non_neg_integer()}
```

Reports whether a call may proceed now.

Returns `:ok` when the call is within the limit, or `{:wait, milliseconds}`
when it is not. Backends that can compute a real time-to-next-window should do
so, so that callers sleep for the right amount of time rather than an estimate.

# `init`

```elixir
@callback init(service(), options :: keyword()) :: {:ok, config()}
```

Prepares the rate limiter for `service`.

Receives the validated `:rate_limit` options (`:limit` and `:per`) with any
backend-specific options merged in.

# `peek`

```elixir
@callback peek(service(), config()) :: :ok | {:wait, non_neg_integer()}
```

Reports whether a call would be admitted right now, **without consuming**
anything.

Returns the same values as `c:check/2`, but must leave the limiter's state
untouched so that callers can ask speculatively. Where a backend can only
answer approximately, prefer erring toward `{:wait, _}` — a caller that skips
work it could have done is cheaper than one that floods a service it should
have waited for.

# `reset`

```elixir
@callback reset(service(), config()) :: :ok
```

Discards the limiter's recorded usage, returning it to a full budget.

The counterpart to `c:ExternalService.CircuitBreaker.reset/2`. Mostly useful
between tests, where a bucket drained by one test would otherwise throttle the
next, but also for clearing a limiter after an operational intervention.

# `peek`

```elixir
@spec peek(service()) :: :ok | {:wait, non_neg_integer()}
```

Reports whether a call to `service` would be admitted right now, without
consuming any of its budget.

A service with no rate limit configured — including one that was never started
— answers `:ok`, since nothing is holding calls back. Use
`ExternalService.available?/1` if you need to know whether a service is ready
to use.

# `request`

```elixir
@spec request(service()) ::
  :ok
  | {:error,
     ExternalService.RateLimited.t() | ExternalService.ServiceNotStarted.t()}
```

Consumes one call's worth of `service`'s rate limit without running anything.

For traffic that reaches the service by some path other than
`ExternalService.call/3` and should still count against the budget. Blocks
according to the service's `:wait` setting, exactly as a guarded call would,
and returns `ExternalService.RateLimited` if that budget runs out.

# `reset`

```elixir
@spec reset(service()) :: :ok | {:error, :not_found}
```

Discards `service`'s recorded rate limit usage, returning it to a full budget.

Symmetric with `ExternalService.CircuitBreaker.reset/1`. A service with no rate
limit configured answers `:ok`, since there is nothing to reset; one that was
never started answers `{:error, :not_found}`.

Chiefly useful between tests — a bucket drained by one test throttles the next,
and nothing clears it automatically. `ExternalService.reset_all/1` resets the
breaker and the limiter together, which is usually what a `setup` block wants.

---

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