# GlobalRateLimiter usage rules

## Defining limits

- Build limits with `GlobalRateLimiter.Limit.new/2`, which validates its
  options. Do not construct the `%GlobalRateLimiter.Limit{}` struct literal
  directly.
- `:window` is a positive number of milliseconds; `:limit` is a non-negative
  maximum count.
- The resource may be any Erlang term. Include every dimension that needs an
  independent limit, such as an action and user, tenant, API key, or IP
  address.
- Keep resource terms deterministic across nodes. Do not include a PID,
  reference, or node-local value in a resource intended to be cluster-wide.
- The whole limit — resource, window, and limit — identifies the tracked
  consumption group. The same resource with a different window or limit is
  tracked independently, and changing a limit's window or limit value starts a
  fresh consumption history. Use stable values for each logical limiter.

```elixir
limit =
  GlobalRateLimiter.Limit.new({:password_challenge, client_ip},
    window: 60_000,
    limit: 3
  )
```

## Consuming

- Use `GlobalRateLimiter.try_consume/2` when the operation must either start
  immediately or be rejected. It returns `{:ok, remaining}`,
  `{:error, {:rate_limited, retry_after_ms}}` when current consumption leaves
  insufficient capacity, or `{:error, :count_exceeds_limit}` when the
  requested count can never fit.
- Use `GlobalRateLimiter.consume/3` when the caller may block until capacity
  is available. Prefer a finite timeout in request-handling processes.
- A successful call has already consumed capacity when it returns
  `{:ok, remaining}`. Do not call `try_consume/2` again after a successful
  consume.
- Do not implement check-then-consume logic with `remaining/1`. It is an
  observational snapshot; `try_consume/2` and `consume/3` perform the atomic
  capacity check.
- Counts must be positive. A count greater than the limit always returns
  `{:error, :count_exceeds_limit}`.

```elixir
case GlobalRateLimiter.try_consume(limit, 1) do
  {:ok, _remaining} -> perform_operation()
  {:error, {:rate_limited, retry_after_ms}} -> retry_after(retry_after_ms)
  {:error, :count_exceeds_limit} -> reject_invalid_count()
end
```

```elixir
case GlobalRateLimiter.consume(limit, 1, 5_000) do
  {:ok, _remaining} -> perform_operation()
  {:error, :timeout} -> retry_later()
  {:error, :count_exceeds_limit} -> reject_operation()
end
```

## Waiting behavior

- Waiting callers are monitored. If a caller exits while queued, its entry is
  removed without consuming capacity.
- A timeout leaves no reservation behind. A timeout of `0` succeeds only when
  capacity is immediately available. There is no default timeout; pass
  `:infinity` explicitly to wait without a deadline.
- FIFO ordering applies only to waiters for the same limit on one node.
  Immediate consumers and waiters on other nodes may overtake them.
- `retry_after_ms` is a hint from the current cluster snapshot, not a
  reservation. Another caller may consume the capacity first.

## Distribution and operational limits

- GlobalRateLimiter starts with the application; do not manually start its
  `:pg` scope, registry, supervisor, counters, or waiters.
- Cluster-wide enforcement requires connected Erlang nodes. Consumption is
  coordinated with `:global` and discovered through `:pg`.
- State is in memory. Node or application restarts discard that node's
  counters, and a network partition allows each partition to enforce its own
  view of the limit.
- GlobalRateLimiter is intended for low-traffic keys. Do not use it as a
  high-throughput or durable distributed quota system.
