# `Ankusa.RetryPolicy`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/retry_policy.ex#L1)

Backoff and give-up rules for dispatch.

A policy decides *how long to wait* (`c:backoff/2`) and may give up early
(`:give_up`). The wall-clock horizon is not the policy's to enforce: dispatch
gives every delivery `max_elapsed_ms` from its first attempt (the
`:max_elapsed_ms` policy option, default 24 hours; a source's `retry:`
overrides it) and dead-letters a delivery that is still failing once that has
passed, whatever `c:backoff/2` answers. `schedule/5` is that rule as a pure
function: no retry it returns is due later than the horizon.

# `backoff`

```elixir
@callback backoff(attempt :: pos_integer(), opts :: keyword()) ::
  {:retry, non_neg_integer()} | :give_up
```

Given the attempt number (1-based, the attempt that just failed), return
`{:retry, delay_ms}` to retry after a delay, or `:give_up` to dead-letter.

# `deadline`

```elixir
@spec deadline(integer(), {module(), keyword()}) :: integer()
```

When a delivery whose first attempt was at `first_attempt_at` runs out of time.

# `default_max_elapsed_ms`

```elixir
@spec default_max_elapsed_ms() :: pos_integer()
```

The default horizon: 24 hours from a delivery's first attempt.

# `for_source`

```elixir
@spec for_source({module(), keyword()}, keyword() | nil) :: {module(), keyword()}
```

The policy a source runs under: the instance's `{module, opts}` with the
source's `retry:` options laid over `opts`.

# `max_elapsed_ms`

```elixir
@spec max_elapsed_ms(keyword()) :: pos_integer()
```

The horizon, in milliseconds, of a policy's `opts`.

# `normalize_opts!`

```elixir
@spec normalize_opts!(nil | keyword() | map(), String.t()) :: keyword() | nil
```

Validate a per-source `retry:` (a keyword list or a map with atom keys) and
return it as a keyword list. `nil` is no override. Raises `ArgumentError`
naming `label` for an unknown key or a value out of range.

# `schedule`

```elixir
@spec schedule(
  {module(), keyword()},
  Ankusa.Sink.class(),
  %{
    :attempts =&gt; pos_integer(),
    :first_attempt_at =&gt; integer(),
    optional(atom()) =&gt; term()
  },
  integer(),
  pos_integer()
) :: {:retry, integer()} | :give_up
```

What to do with a delivery that just failed, as the absolute time of its
next attempt (`now` is the clock in milliseconds): `{:retry, at}` or
`:give_up`.

`class` is `Ankusa.Sink.classify/1`'s answer. A `{:permanent, _}` failure, a
delivery that is past its horizon (`first_attempt_at + max_elapsed_ms`) and a
policy that answers `:give_up` are all `:give_up`. A `{:retry_after, ms, _}`
failure waits the later of the policy's backoff and `ms`, with `ms` capped at
`max_retry_after_ms`. `at` is never later than the horizon, so the last
attempt runs on it and the one after that is the dead letter.

# `validate_opt!`

```elixir
@spec validate_opt!(atom(), term(), String.t()) :: :ok
```

The range check of one retry option, shared by `normalize_opts!/2` and
`Ankusa.Config`. `label` is the dotted path of the `retry` block. Raises
`ArgumentError`.

---

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