// gRPC transparent-retry policy (the `retryPolicy` of a service config's method
// config): how many attempts a call may make, which `grpc-status` codes permit
// another attempt, and the exponential backoff between them. This is pure decision
// logic — all-backend and total — that the async `Channel` drives: it decides
// *whether* and *how long to wait*, the socket layer re-issues and sleeps.
///|
/// A method's retry policy. `max_attempts` counts the first try plus every retry
/// (gRPC caps it at 5); a call is retried only while attempts remain and its status
/// is in `retryable`. Backoff starts at `initial_backoff_millis` and grows by
/// `backoff_multiplier` each retry, capped at `max_backoff_millis`.
pub(all) struct RetryPolicy {
max_attempts : Int
initial_backoff_millis : Int
max_backoff_millis : Int
backoff_multiplier : Double
retryable : Array[Int]
}
///|
/// A conventional default: up to 3 attempts, 100 ms initial backoff doubling to a
/// 1 s cap, retrying only `UNAVAILABLE` — the code a transient, safe-to-retry
/// transport failure carries.
pub fn RetryPolicy::default() -> RetryPolicy {
{
max_attempts: 3,
initial_backoff_millis: 100,
max_backoff_millis: 1000,
backoff_multiplier: 2.0,
retryable: [Status::code(Unavailable)],
}
}
///|
/// Whether a call that has made `attempts_made` attempts (the first is 1) and got
/// `status_code` should be tried again: attempts must remain, and the status must be
/// non-OK and listed as retryable.
pub fn RetryPolicy::should_retry(
self : RetryPolicy,
status_code : Int,
attempts_made : Int,
) -> Bool {
// gRPC caps the effective attempt count at 5 regardless of the configured value.
let cap = if self.max_attempts > 5 { 5 } else { self.max_attempts }
if attempts_made >= cap || status_code == 0 {
return false
}
for code in self.retryable {
if code == status_code {
return true
}
}
false
}
///|
/// The backoff cap before the `retry_index`-th retry (the first retry is 1):
/// `min(initial * multiplier^(retry_index-1), max)`. gRPC sleeps a value drawn
/// uniformly from `[0, cap]`; the async driver applies that jitter, so this returns
/// the deterministic upper bound (which is also what a jitter-free driver sleeps).
pub fn RetryPolicy::backoff_millis(
self : RetryPolicy,
retry_index : Int,
) -> Int {
let mut backoff = self.initial_backoff_millis.to_double()
for _i = 1; _i < retry_index; _i = _i + 1 {
backoff = backoff * self.backoff_multiplier
if backoff >= self.max_backoff_millis.to_double() {
return self.max_backoff_millis
}
}
let capped = if backoff >= self.max_backoff_millis.to_double() {
self.max_backoff_millis
} else {
backoff.to_int()
}
// Never sleep a negative duration, even if a policy is misconfigured with a negative
// backoff.
if capped < 0 {
0
} else {
capped
}
}