use std::time::Duration;
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_attempts: usize,
pub per_attempt_deadline: Duration,
pub overall_deadline: Duration,
pub base_backoff: Duration,
pub leader_ttl: Duration,
}
impl RetryPolicy {
pub const MAX_BACKOFF: Duration = Duration::from_secs(5);
}
impl Default for RetryPolicy {
fn default() -> Self {
RetryPolicy {
max_attempts: 5,
per_attempt_deadline: Duration::from_secs(2),
overall_deadline: Duration::from_secs(10),
base_backoff: Duration::from_millis(50),
leader_ttl: Duration::from_secs(30),
}
}
}
pub(crate) fn is_transport_failure(error: &crate::error::ClientError) -> bool {
use crate::error::ClientError;
match error {
ClientError::Rpc(status) => matches!(
status.code(),
tonic::Code::Unavailable | tonic::Code::DeadlineExceeded
),
ClientError::Transport(_) | ClientError::TransportFanout(_) => true,
ClientError::NoReachableEndpoints
| ClientError::InvalidEndpoint(_)
| ClientError::InvalidCount(_)
| ClientError::Connector(_)
| ClientError::DriverGone => false,
}
}
pub(crate) fn should_backoff(error: &crate::error::ClientError) -> bool {
is_transport_failure(error)
}
pub(crate) fn jittered_backoff(base: Duration, attempt: u32) -> Duration {
if base.is_zero() {
return Duration::ZERO;
}
let shift = attempt.min(20);
let upper = base
.saturating_mul(1u32.checked_shl(shift).unwrap_or(u32::MAX))
.min(RetryPolicy::MAX_BACKOFF);
let upper_micros = upper.as_micros().min(u64::MAX as u128) as u64;
if upper_micros == 0 {
return Duration::ZERO;
}
let picked = rand::random_range(0..=upper_micros);
Duration::from_micros(picked)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_documented_values() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_attempts, 5);
assert_eq!(policy.per_attempt_deadline, Duration::from_secs(2));
assert_eq!(policy.overall_deadline, Duration::from_secs(10));
assert_eq!(policy.base_backoff, Duration::from_millis(50));
assert_eq!(policy.leader_ttl, Duration::from_secs(30));
}
#[test]
fn jittered_backoff_zero_base_returns_zero() {
for attempt in 0..5 {
assert_eq!(jittered_backoff(Duration::ZERO, attempt), Duration::ZERO);
}
}
#[test]
fn jittered_backoff_respects_window_at_attempt_zero() {
let base = Duration::from_millis(10);
for _ in 0..200 {
let picked = jittered_backoff(base, 0);
assert!(
picked <= base,
"attempt 0 must be in [0, base]; got {picked:?} > {base:?}"
);
}
}
#[test]
fn jittered_backoff_respects_window_at_higher_attempts() {
let base = Duration::from_millis(10);
for attempt in 1..6u32 {
let upper = base
.saturating_mul(1u32 << attempt)
.min(RetryPolicy::MAX_BACKOFF);
for _ in 0..200 {
let picked = jittered_backoff(base, attempt);
assert!(
picked <= upper,
"attempt {attempt} must be in [0, {upper:?}]; got {picked:?}"
);
}
}
}
#[test]
fn jittered_backoff_caps_at_max_backoff() {
let base = Duration::from_secs(10);
for _ in 0..200 {
let picked = jittered_backoff(base, 5);
assert!(
picked <= RetryPolicy::MAX_BACKOFF,
"saturating cap must hold; got {picked:?} > {:?}",
RetryPolicy::MAX_BACKOFF
);
}
}
#[test]
fn jittered_backoff_produces_a_distribution_not_a_constant() {
let base = Duration::from_millis(100);
let mut min = Duration::MAX;
let mut max = Duration::ZERO;
for _ in 0..200 {
let picked = jittered_backoff(base, 3);
if picked < min {
min = picked;
}
if picked > max {
max = picked;
}
}
assert!(
max > min,
"jittered_backoff must produce a distribution; min={min:?}, max={max:?}"
);
}
#[test]
fn should_backoff_matches_documented_status_codes() {
use crate::error::ClientError;
assert!(should_backoff(&ClientError::Rpc(
tonic::Status::unavailable("u")
)));
assert!(should_backoff(&ClientError::Rpc(
tonic::Status::deadline_exceeded("d")
)));
assert!(!should_backoff(&ClientError::Rpc(tonic::Status::internal(
"i"
))));
assert!(!should_backoff(&ClientError::Rpc(
tonic::Status::failed_precondition("fp")
)));
assert!(!should_backoff(&ClientError::NoReachableEndpoints));
assert!(should_backoff(&ClientError::TransportFanout("t".into())));
assert!(!should_backoff(&ClientError::InvalidEndpoint("e".into())));
assert!(!should_backoff(&ClientError::InvalidCount(0)));
assert!(!should_backoff(&ClientError::DriverGone));
}
}