Control.Retry
module Control.Retry
Bounded retries for operations whose failures can be classified by the application.
A retry is never automatically safe just because an error was temporary. The caller owns the operation and, where necessary, a predicate that excludes permanent failures and non-idempotent work. Policies bound attempts, delay, and optionally total sleep so a dependency cannot stall the program forever.
using Control.Retry
let policy = Retry.exponential(4, 100.milliseconds, 2.seconds)
.withJitter(0.2)
Retry.run(policy, ~retryable?) do
client.get("https://api.example.com/inventory")
end
record Policy
A bounded retry schedule. Attempts includes the initial call. Delays are immutable Duration values and never exceed maximumDelay.
Fields
maximumAttemptsIntegerinitialDelayDurationmultiplierFloatmaximumDelayDurationmaximumElapsedDuration?optionaljitterFractionFloatoptional
type Predicate<E>
Decides whether an application error is eligible for another attempt.
Variants
- abstract
type Sleeper
Performs one scheduled delay. Supplying this callback makes retry tests deterministic without sleeping.
Variants
- abstract
function fixed
Builds a constant-delay retry policy.
Fixed delays are predictable and useful for a local resource expected to become ready shortly. For many clients sharing a remote dependency, prefer exponential backoff with jitter to avoid synchronized retry bursts.
fixed(maximumAttempts, delay) : Integer -> Duration -> Policy
Parameters
maximumAttemptsInteger- total calls including the first
delayDuration- delay before each subsequent call
Returns: Policy — the retry schedule
Examples
Waiting briefly for a local test server to start
let policy = Retry.fixed(5, 50.milliseconds)
function exponential
Builds a doubling backoff capped at maximumDelay.
The first retry waits initialDelay; later delays double until they reach the cap. Add jitter for production network traffic.
exponential : Integer -> Duration -> Duration -> Policy
Parameters
maximumAttemptsInteger- total calls including the first
initialDelayDuration- delay after the first failure
maximumDelayDuration- upper bound for each delay
Returns: Policy — the retry schedule
Examples
Backing off calls to a busy upstream service
Retry.exponential(5, 200.milliseconds, 5.seconds).withJitter(0.25)
make Policy
withMaximumElapsed
Returns the same schedule with a bound on total scheduled sleep time.
The operation's own execution time is not counted; use operation-specific deadlines for that. A retry whose next delay would exceed this bound is not started.
withMaximumElapsed(maximumElapsed)
Parameters
maximumElapsedDuration- maximum cumulative scheduled delay
Returns: Policy — a copied policy with the elapsed bound
Examples
Retry.fixed(5, 1.seconds).withMaximumElapsed(2.seconds).
withJitter
Returns the same schedule with symmetric bounded jitter. A fraction of 0.25 selects each actual delay from 75% through 125% of its scheduled value. Fractions are clamped to 0.0..1.0.
Jitter prevents many workers that failed together from retrying together. It changes delay timing, never the number of attempts or the backoff cap.
withJitter(fraction)
Parameters
fractionFloat- maximum proportional variation on either side
Returns: Policy — a copied policy with bounded jitter
Examples
Spreading retries by up to 20 percent
Retry.exponential(4, 1.seconds, 10.seconds).withJitter(0.2)
function run
Runs operation until it succeeds or exhausts the policy.
The last application error is returned unchanged. This helper performs no network-specific classification: callers decide what operation to wrap.
run(policy, operation) : Policy -> Block<Result<X, E>> -> Result<X, E>
run(policy, operation) : Policy -> Predicate<E> -> Block<Result<X, E>> -> Result<X, E>
Parameters
policyPolicy- the bounded schedule
operationBlock<Result<X,E>>- a fresh attempt
Returns: Result<X,E> — the first success or final failure
Examples
Retrying an idempotent health check
Retry.run(Retry.fixed(3, 250.milliseconds)) do
HTTP.get("https://service.example.com/health")
end
function runWith
Runs with injected error classification and sleeping.
This is the deterministic testing seam: a fake sleeper can record durations or advance a virtual clock. Production callers normally use Retry.run.
runWith : Policy -> Predicate<E> -> Sleeper -> Block<Result<X, E>> -> Result<X, E>
Parameters
policyPolicy- the bounded schedule
sleeperSleeper- performs or records each scheduled delay
operationBlock<Result<X,E>>- a fresh attempt
Returns: Result<X,E> — the first success or final failure
function runWithRandom
Runs with injected sleeping and a random source returning a value in 0.0..1.0. Out-of-range test values are clamped. Production run uses a cryptographically secure backend source; this overload makes jitter specs deterministic.
runWithRandom : Policy -> Predicate<E> -> Sleeper -> Block<Float> -> Block<Result<X, E>> -> Result<X, E>
function attempt
attempt(policy, predicate, sleeper, random, operation, number, delay, elapsed)
module Control.Retry.Retry
The imported public namespace: using Control.Retry then Retry.run(...).
function fixed
Public imported alias of Control.Retry.fixed.
fixed(maximumAttempts, delay) : Integer -> Duration -> Policy
function exponential
Public imported alias of Control.Retry.exponential.
exponential : Integer -> Duration -> Duration -> Policy
function run
Public imported alias of Control.Retry.run.
run(policy, operation) : Policy -> Block<Result<X, E>> -> Result<X, E>
run(policy, operation) : Policy -> Predicate<E> -> Block<Result<X, E>> -> Result<X, E>
function runWith
Deterministic seam with an injected sleeper, primarily for specifications.
runWith : Policy -> Predicate<E> -> Sleeper -> Block<Result<X, E>> -> Result<X, E>
function runWithRandom
Deterministic seam with injected sleeping and random sampling.
runWithRandom : Policy -> Predicate<E> -> Sleeper -> Block<Float> -> Block<Result<X, E>> -> Result<X, E>