# `TypeSafeAPI.Retry`
[🔗](https://github.com/typesend/typesafe_ai/blob/v0.1.0-alpha.3/lib/typesafe_api/retry.ex#L1)

Retry policy mirroring the official TypeSafe SDKs, implemented as a Req step.

Req ships its own retry step, but it counts attempts, not wall time. The
official SDKs enforce a *total* budget per call (30 seconds by default)
covering the first attempt, every retry, and every delay in between. The
budget gates whether another retry is *started*, not how long the attempt it
starts may run, so a call can overrun it by up to one attempt timeout. Even
so, that budget is the difference between "a slow request" and "a request
that pins a worker for two minutes while the API is overloaded", so this
module replaces Req's step with one that knows about the deadline.

## Defaults

| option                     | default             | meaning                                    |
| -------------------------- | ------------------- | ------------------------------------------ |
| `max_retries`              | `2`                 | retries after the first attempt            |
| `backoff_initial`          | `500` ms            | first delay, doubled each retry            |
| `backoff_max`              | `5_000` ms          | ceiling for the exponential delay          |
| `backoff_jitter`           | `0.25`              | fraction randomly subtracted from a delay  |
| `statuses`                 | `408, 429, 500-599` | HTTP statuses that trigger a retry         |
| `respect_retry_after`      | `true`              | honor `retry-after-ms` and `Retry-After`   |
| `retry_after_min`          | `100` ms            | floor on a server-requested delay          |
| `retry_after_max`          | `60_000` ms         | ceiling on a server-requested delay        |
| `retry_connection_errors`  | `:auto`             | retry when the server cannot be reached    |
| `retry_timeout_errors`     | `:auto`             | retry when a single attempt times out      |
| `budget`                   | `30_000` ms         | wall time before another retry starts      |

When both `retry-after-ms` and `Retry-After` are present, `retry-after-ms`
wins, matching the Python SDK. `Retry-After` may be seconds or an HTTP date.
A server-requested delay is clamped into `retry_after_min..retry_after_max`,
so `retry-after: 0`, an empty header, or a date the local clock has already
passed cannot turn the policy into a tight loop against a server that is
already struggling, and a wildly long one cannot pin a worker.

`retry_connection_errors` and `retry_timeout_errors` both default to `:auto`,
which is `true` for `GET` and `false` for `POST`, since a replayed
`POST /v1/systemone` can double-bill an evaluation. A timeout is the *most*
likely failure to have reached the server, so it gets the same guard. A
connection pool checkout timeout is the exception: nothing was sent, so it is
always retried.

A retry whose delay would reach or exceed the remaining budget is not
attempted; the last error is returned instead. Delays are milliseconds.

See the [errors and retries guide](errors_and_retries.md) for the full
picture: the end-to-end retry timeline, why connection errors are not
retried the same way as server-signalled ones, how the budget interacts
with a single attempt's timeout, and how these option names map to the
official SDKs'.

## Building a policy

    TypeSafeAPI.Retry.new(max_retries: 5, budget: 60_000)
    TypeSafeAPI.Retry.new(max_retries: 0)   # never retry

Pass the result as `retry:` to `TypeSafeAPI.new/1` or to a single call.

# `t`

```elixir
@type t() :: %TypeSafeAPI.Retry{
  backoff_initial: non_neg_integer(),
  backoff_jitter: float(),
  backoff_max: non_neg_integer(),
  budget: non_neg_integer() | nil,
  clock_fun: (-&gt; integer()),
  max_retries: non_neg_integer(),
  respect_retry_after: boolean(),
  retry_after_max: non_neg_integer(),
  retry_after_min: non_neg_integer(),
  retry_connection_errors: boolean() | :auto,
  retry_timeout_errors: boolean() | :auto,
  sleep_fun: (non_neg_integer() -&gt; term()),
  statuses: MapSet.t(pos_integer())
}
```

# `attach`

```elixir
@spec attach(Req.Request.t(), t()) :: Req.Request.t()
```

Attaches the retry policy to a `Req.Request`.

Disables Req's built-in retry step and registers this module's step for both
responses and exceptions. The retry count is exposed afterwards through
`retry_count/1`.

The policy is resolved against the request's method with `for_method/2`
first, so an `:auto` `retry_connection_errors` becomes a boolean here.

# `backoff`

```elixir
@spec backoff(t(), pos_integer()) :: non_neg_integer()
```

Exponential backoff with jitter for retry number `attempt` (1-based).

The delay starts at `backoff_initial`, doubles each retry, and is capped at
`backoff_max`. Jitter subtracts up to `backoff_jitter` of the delay, so the
result is always between `delay * (1 - jitter)` and `delay`.

# `delay`

```elixir
@spec delay(t(), Req.Response.t() | Exception.t(), pos_integer()) :: non_neg_integer()
```

The delay in milliseconds before retry number `attempt` (1-based), given the
response or exception that triggered it.

Uses the server's `retry-after-ms` or `Retry-After` header when present and
`respect_retry_after` is set; otherwise exponential backoff with jitter.

A server-requested delay is clamped into `retry_after_min` at the bottom and
`retry_after_max` at the top, and otherwise honored as given, even when it is
shorter than the exponential backoff would have been: the server knows its own
load better than a schedule does. The floor matters: `retry-after: 0`, an
empty header and an HTTP date a skewed clock reads as past all parse to zero,
and honoring that verbatim would hammer a server that is already struggling
while the wall-clock budget never advances.

# `for_method`

```elixir
@spec for_method(t(), atom()) :: t()
```

Resolves method-dependent defaults against the HTTP method of a request.

Turns `retry_connection_errors: :auto` and `retry_timeout_errors: :auto` into
`true` for methods that are safe to replay (`GET`, `HEAD`, `OPTIONS`) and
`false` for the rest, `POST` included. An explicit boolean is left alone.
Called by `attach/2`.

# `merge`

```elixir
@spec merge(t() | keyword(), t() | keyword()) :: t()
```

Layers `overrides` onto an existing policy.

A per-call `retry: [max_retries: 1]` should change one setting, not reset
every other one to the library default, so `TypeSafeAPI.HTTP` merges rather
than rebuilds. Pass a `%TypeSafeAPI.Retry{}` instead of a keyword list to
replace a policy wholesale.

    client.retry |> TypeSafeAPI.Retry.merge(max_retries: 1)

Note the asymmetry, which is the whole point: a keyword list is a set of
changes, a struct is a finished policy. A layer that wants to stay mergeable
has to stay a keyword list, which is what `validate!/1` is for.

# `new`

```elixir
@spec new(t() | keyword()) :: t()
```

Builds a policy from a keyword list, validating every option.

Accepts an existing `%TypeSafeAPI.Retry{}` unchanged so callers can pass either.

## Options

* `:max_retries` (`t:non_neg_integer/0`) - Maximum retries after the initial attempt; `0` disables retries. The default value is `2`.

* `:backoff_initial` (`t:non_neg_integer/0`) - First backoff delay in milliseconds, doubled each retry. The default value is `500`.

* `:backoff_max` (`t:non_neg_integer/0`) - Maximum backoff delay in milliseconds; `0` disables backoff. The default value is `5000`.

* `:backoff_jitter` - Fraction of each backoff delay randomly subtracted, between 0 and 1. The default value is `0.25`.

* `:statuses` (list of `t:pos_integer/0`) - HTTP status codes that are retried. The default value is `[500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, ...]`.

* `:respect_retry_after` (`t:boolean/0`) - Whether to honor `Retry-After` and `retry-after-ms` response headers. The default value is `true`.

* `:retry_after_min` (`t:non_neg_integer/0`) - Floor in milliseconds for a delay the server asked for. `retry-after: 0`, an empty header and a past HTTP date all parse to zero; without a floor they would retry in a tight loop while the wall-clock budget never advances. The default value is `100`.

* `:retry_after_max` (`t:non_neg_integer/0`) - Ceiling in milliseconds for a delay the server asked for. A longer `Retry-After` is clamped to this rather than pinning the caller. The default value is `60000`.

* `:retry_connection_errors` - Whether to retry when the request cannot reach the server. `:auto` retries on `GET` but not on `POST`, whose replay can double-bill an evaluation. The default value is `:auto`.

* `:retry_timeout_errors` - Whether to retry when a single attempt times out. `:auto` retries on `GET` but not on `POST`, whose replay can double-bill an evaluation. A connection pool checkout timeout is retried either way: nothing was sent. The default value is `:auto`.

* `:budget` - Wall-clock budget in milliseconds per call. A retry is not started once the elapsed time plus its delay would reach it, so a call can overrun by one attempt timeout. `nil` disables the limit. The default value is `30000`.

# `option_type`

```elixir
@spec option_type() :: {:or, [atom() | {:struct, module()}]}
```

The `NimbleOptions` type for a `:retry` option: a keyword list for `new/1`
or a ready policy. Shared by every schema that accepts one.

# `retry_after_ms`

```elixir
@spec retry_after_ms(Req.Response.t()) :: non_neg_integer() | nil
```

Reads the server's requested wait from `retry-after-ms` (milliseconds) or
`Retry-After` (seconds or an HTTP date). Returns `nil` if neither is usable.

# `retry_count`

```elixir
@spec retry_count(Req.Request.t()) :: non_neg_integer()
```

Number of retries performed on a request that has been run.

# `retryable?`

```elixir
@spec retryable?(t(), Req.Response.t() | Exception.t()) :: boolean()
```

Whether the policy would retry the given response or exception.

An unresolved `:auto` counts as `false`; run the policy through `for_method/2`
(as `attach/2` does) to resolve it. A pool checkout timeout is the one case
that ignores both flags: the request never left the process.

# `validate!`

```elixir
@spec validate!(t() | keyword()) :: t() | keyword()
```

Validates a `:retry` option without turning it into a policy.

For callers that validate per-call options once and reuse them for many
requests: `new/1` would fill in every default, and the result could no
longer be told apart from a policy the caller meant to impose wholesale.
This raises on a bad option exactly as `new/1` does, and hands back
something `merge/2` can still layer.

    iex> TypeSafeAPI.Retry.validate!(max_retries: 1)
    [max_retries: 1]

---

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