automata/retry

Types

Position of a single try inside a retry sequence.

Counts start at 1: the very first invocation is attempt 1, the first retry is attempt 2, and so on. max_attempts therefore means “the total number of tries we are willing to perform” — a max_attempts: 5 policy gives up after the fifth failed attempt.

pub type Attempt =
  Int

Stateful retry context.

Bundles the immutable Policy, the number of attempts already completed, the cumulative delay charged to the caller so far, and the deterministic PRNG state used by jittered policies. opaque because callers must not forge an attempt counter or rewind the PRNG mid-sequence.

pub opaque type Context

Result of asking a Context what to do after a failure.

Retry carries the delay the caller should wait before the next attempt and the Context to use when calling decide again. GiveUp carries the structured reason the sequence ended.

pub type Decision {
  Retry(delay: Duration, next: Context)
  GiveUp(reason: GiveUpReason)
}

Constructors

A non-negative span of time, opaque so it can only be built through the validating from_* smart constructors.

Values are stored as integer milliseconds and are guaranteed to lie in [0, max_safe_milliseconds].

pub opaque type Duration

Classification a caller assigns to a failed try.

automata/retry does not interpret error values. The caller decides whether a particular failure is worth re-trying (Transient) or hopeless (Permanent), and the retry module honours that decision.

pub type FailureKind {
  Transient
  Permanent
}

Constructors

  • Transient
  • Permanent

Reason a retry sequence stops.

Each variant carries enough structured context that an LLM or a human operator can understand the decision without parsing prose.

pub type GiveUpReason {
  PolicyDisallowsRetry
  MaxAttemptsReached(attempts: Int, limit: Int)
  PermanentFailureSignaled(at_attempt: Int)
  DelayOverflow(at_attempt: Int)
}

Constructors

  • PolicyDisallowsRetry

    The policy is no_retry; failure ends the sequence immediately.

  • MaxAttemptsReached(attempts: Int, limit: Int)

    We have exhausted the policy’s max_attempts budget.

  • PermanentFailureSignaled(at_attempt: Int)

    The caller signalled Permanent failure.

  • DelayOverflow(at_attempt: Int)

    The next delay would exceed max_safe_milliseconds and was refused before any precision was lost.

Strategy used to spread the base delay computed by a policy.

Kept as a regular sum (not opaque) so that decide can dispatch on it. New strategies (for example a bounded-percentage jitter) would be additive and require existing pattern matches to be exhaustive again.

pub type Jitter {
  NoJitter
  FullJitter
  EqualJitter
}

Constructors

  • NoJitter

    Use the policy’s base delay as-is.

  • FullJitter

    Spread uniformly across [0, base] (AWS “full jitter”).

  • EqualJitter

    Spread uniformly across [base / 2, base] (AWS “equal jitter”).

A retry policy: pure data describing how to retry a failed operation without itself performing any I/O.

opaque so the only way to construct a value is through the validating smart constructors (no_retry, fixed, exponential, capped_exponential). This rules out impossible combinations such as max_attempts <= 0 or a cap below the initial delay, and lets us add new variants (decorrelated jitter, custom backoff, …) without breaking pattern matches in downstream code.

pub opaque type Policy

Construction-time errors for Duration and Policy.

pub type RetryError {
  NegativeDuration(unit: String, actual: Int)
  DurationOverflow(unit: String, actual: Int)
  MaxAttemptsMustBePositive(actual: Int)
  MultiplierMustBeAtLeastTwo(actual: Int)
  InitialDelayMustBePositive(actual_milliseconds: Int)
  CapMustNotBeLessThanInitial(
    cap_milliseconds: Int,
    initial_milliseconds: Int,
  )
}

Constructors

  • NegativeDuration(unit: String, actual: Int)
  • DurationOverflow(unit: String, actual: Int)
  • MaxAttemptsMustBePositive(actual: Int)
  • MultiplierMustBeAtLeastTwo(actual: Int)
  • InitialDelayMustBePositive(actual_milliseconds: Int)
  • CapMustNotBeLessThanInitial(
      cap_milliseconds: Int,
      initial_milliseconds: Int,
    )

Values

pub fn capped_exponential(
  initial initial: Duration,
  multiplier multiplier: Int,
  cap cap: Duration,
  max_attempts max_attempts: Int,
) -> Result(Policy, RetryError)

Build an exponential backoff that saturates at cap. Equivalent to exponential with an extra ceiling applied to every computed delay.

pub fn capped_exponential_ms(
  initial_ms initial_ms: Int,
  multiplier multiplier: Int,
  cap_ms cap_ms: Int,
  max_attempts max_attempts: Int,
) -> Result(Policy, RetryError)

Build a capped exponential policy directly from raw millisecond integers, skipping the explicit from_milliseconds calls that capped_exponential/4 would otherwise require. Useful when both the initial delay and the cap are compile-time literals — the common case at a call site.

Validation runs in the same order as capped_exponential/4: the initial_ms duration is constructed first, then cap_ms, then the underlying capped_exponential validates multiplier, max_attempts, and the cap >= initial invariant. A bad value at any step short- circuits with the corresponding RetryError.

Callers that already hold runtime-derived Duration values (for example, from a config parser that ran from_minutes) should keep using capped_exponential/4 and pass the values through unchanged.

pub fn cumulative_delay(ctx ctx: Context) -> Duration

Total of the delays the policy has handed out so far. Useful for applying an upper-bound deadline at the call site.

pub fn current_attempt(ctx ctx: Context) -> Int

Number of attempts already completed in this sequence.

pub fn decide(
  ctx ctx: Context,
  failure failure: FailureKind,
) -> Decision

Decide what to do after a failed attempt.

Permanent short-circuits regardless of the policy. Transient consults the policy: it may yield a retry delay, the context to use next, or a structured reason to stop.

pub fn duration_milliseconds(duration duration: Duration) -> Int

Recover the raw millisecond value from a duration.

pub fn duration_seconds(duration duration: Duration) -> Int

Recover the duration in whole seconds, truncating any sub-second remainder.

pub fn duration_to_string(duration duration: Duration) -> String

Render a duration as "<n>ms".

pub fn exponential(
  initial initial: Duration,
  multiplier multiplier: Int,
  max_attempts max_attempts: Int,
) -> Result(Policy, RetryError)

Build an unbounded exponential backoff: the next delay is initial * multiplier ^ (attempt - 1), where attempt is the upcoming try number (1-based).

pub fn fixed(
  delay delay: Duration,
  max_attempts max_attempts: Int,
) -> Result(Policy, RetryError)

Build a fixed-delay policy: every retry waits exactly delay, up to max_attempts total tries.

pub fn from_milliseconds(
  milliseconds milliseconds: Int,
) -> Result(Duration, RetryError)

Build a duration from milliseconds.

pub fn from_minutes(
  minutes minutes: Int,
) -> Result(Duration, RetryError)

Build a duration from minutes.

pub fn from_seconds(
  seconds seconds: Int,
) -> Result(Duration, RetryError)

Build a duration from seconds.

pub fn next_delay(
  ctx ctx: Context,
  failure failure: FailureKind,
) -> Result(Duration, GiveUpReason)

Convenience accessor: the delay component of decide, or the give-up reason as Error.

Does not advance the context — call decide if you intend to act on the answer.

pub fn no_retry() -> Policy

Build a policy that never retries. Any failure (transient or permanent) ends the sequence on the first call to decide.

pub fn policy(ctx ctx: Context) -> Policy

Recover the policy a context was started from.

pub fn should_retry(
  ctx ctx: Context,
  failure failure: FailureKind,
) -> Bool

Convenience predicate: True when decide would return Retry.

Does not advance the context — call decide if you intend to act on the answer.

pub fn start(policy policy: Policy, seed seed: Int) -> Context

Begin a new retry sequence.

seed deterministically drives any jitter the policy applies. Use the same seed twice and you get the same delay sequence, on the BEAM and on the JavaScript target alike.

pub fn with_jitter(
  policy policy: Policy,
  jitter jitter: Jitter,
) -> Policy

Decorate an existing policy with a jitter strategy.

no_retry is unaffected (jitter has nothing to spread).

Search Document