///|
/// The statuses retried unless told otherwise: 408, 429, 500, 502, 503 and
/// 504, as Elixir Req's `:safe_transient` and ky do.
let default_retry_statuses : Array[Int32] = [408, 429, 500, 502, 503, 504]
///|
/// The methods retried unless told otherwise: those RFC 9110 ยง9.2.2 calls
/// idempotent, as urllib3's `Retry.DEFAULT_ALLOWED_METHODS` and ky do.
let default_retry_methods : Array[String] = [
"GET", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE",
]
///|
/// When a call is sent again, and how long it waits first.
///
/// A call is sent again, up to `max_retries` times, when its method is one
/// of `methods` and the response's status is one of `statuses` or sending
/// failed with `Timeout` or `Network`; and whatever its method when nothing
/// reached the server (`ReqError::is_unsent`; a proxy refusal only with one
/// of `statuses`). A body that streams is never sent twice. The wait is a
/// `Retry-After` the response gives, in seconds or as an HTTP date on the
/// client's clock, unless it asks for more than `max_retry_after`, in which
/// case the response is returned as it is; otherwise it is drawn between 0
/// and `base_delay * 2 ^ retry`, capped at `max_delay` ("full jitter").
///
/// ```mbt check
/// test {
/// let client = @req.Client::new(retry=@req.Retry::new(max_retries=5))
/// client.close()
/// let client = @req.Client::new(retry=@req.Retry::none())
/// client.close()
/// }
/// ```
pub struct Retry {
priv max_retries : Int32
priv statuses : Array[Int32]
priv methods : Array[String]
priv max_retry_after : Int32
priv base_delay : Int32
priv max_delay : Int32
}
///|
/// A retry policy; the defaults are those of `Client::new`.
///
/// - `max_retries`: sends after the first, 3 by default.
/// - `statuses`: response statuses worth another try, by default 408, 429,
/// 500, 502, 503 and 504.
/// - `methods`: methods sent again after such a status or a `Timeout` or
/// `Network` failure, by default GET, HEAD, OPTIONS, PUT, DELETE and TRACE.
/// - `max_retry_after`: the longest `Retry-After` honoured, in milliseconds,
/// 60 s by default; a server asking for more gets no retry.
/// - `base_delay`, `max_delay`: milliseconds of the backoff, 1 s and 30 s by
/// default.
pub fn Retry::new(
max_retries~ : Int32 = 3,
statuses~ : Array[Int32] = default_retry_statuses,
methods~ : Array[String] = default_retry_methods,
max_retry_after~ : Int32 = 60_000,
base_delay~ : Int32 = 1000,
max_delay~ : Int32 = 30_000,
) -> Retry {
{
max_retries: max_retries.max(0),
statuses: [..statuses],
methods: methods.map(String::to_upper),
max_retry_after: max_retry_after.max(0),
base_delay: base_delay.max(0),
max_delay: max_delay.max(0),
}
}
///|
/// A policy that never sends a call again.
pub fn Retry::none() -> Retry {
Retry::new(max_retries=0)
}
///|
/// The wait in milliseconds before sending `request` again, after `outcome`
/// was the result of retry number `retries` (0 for the first send), or
/// `None` to keep the outcome. `now_ms` reads an HTTP-date `Retry-After`;
/// `random`, in `[0, 1)`, draws the backoff.
fn Retry::delay_ms(
self : Retry,
request : Request,
outcome : Result[Response, ReqError],
retries : Int32,
now_ms : Int64,
random : Double,
) -> Int32? {
guard retries < self.max_retries && request.body.is_replayable() else {
return None
}
guard self.retryable(request, outcome) else { return None }
let server = match outcome {
Ok(response) =>
response.headers
.get("retry-after")
.bind(value => @runtime.parse_retry_after(value, now_unix_ms=now_ms))
Err(_) => None
}
match server {
Some(ms) if ms > self.max_retry_after => None
Some(ms) => Some(ms)
None => Some(full_jitter(self.base_delay, self.max_delay, retries, random))
}
}
///|
/// Whether `outcome` is worth sending `request` again, leaving aside the
/// count and the body.
fn Retry::retryable(
self : Retry,
request : Request,
outcome : Result[Response, ReqError],
) -> Bool {
let idempotent = self.methods.contains(request.http_method.to_upper())
match outcome {
Ok(response) => idempotent && self.statuses.contains(response.status)
// A proxy that refuses with 403 or 407 refuses again; 502 and 504 may
// pass.
Err(Proxy(status~, ..)) => self.statuses.contains(status)
Err(error) if error.is_unsent() => true
Err(Timeout(..) | Network(..)) => idempotent
Err(_) => false
}
}
///|
/// A wait drawn uniformly between 0 and `base * 2 ^ retries`, capped at
/// `max` (AWS's "full jitter"). `random` is clamped into `[0, 1)`.
fn full_jitter(
base : Int32,
max : Int32,
retries : Int32,
random : Double,
) -> Int32 {
let cap = max.to_double()
let mut ceiling = base.to_double().min(cap)
for _ in 0..= 0.0 { random.min(0.9999999999999999) } else { 0.0 }
(ceiling * random).to_int()
}
///|
/// The step that sends a request again as `retry` says, waiting on `clock`.
/// Each send gets its own copy of the headers, so later steps start from the
/// same request every time; a response that is not kept is closed.
fn retry_step(
retry : Retry,
clock : &@clock.Clock,
random : () -> Double,
) -> Step {
(request, next) => {
for retries = 0 {
let sent = { ..request, headers: copy_headers(request.headers), }
let outcome : Result[Response, ReqError] = try next(sent) catch {
error => Err(error)
} noraise {
response => Ok(response)
}
let delay = retry.delay_ms(
request,
outcome,
retries,
clock.now_unix_ms(),
random(),
)
guard delay is Some(delay) else {
match outcome {
Ok(response) => return response
Err(error) => raise error
}
}
if outcome is Ok(response) {
response.close()
}
clock.sleep(delay)
continue retries + 1
}
}
}