pub const DEFAULT_MAX_RETRY_EXCEPTION_ELAPSED_TIME: u64 = 60 * 60;
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_retries: Option<u32>,
pub initial_backoff_ms: u64,
pub max_backoff_ms: Option<u64>,
pub backoff_multiplier: f64,
pub jitter_ms: u64,
pub min_jitter_threshold_ms: u64,
pub recognize_server_retry_delay: bool,
pub max_server_retry_delay_ms: Option<u64>,
pub max_retry_exception_elapsed_time_ms: Option<u64>,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: Some(15),
initial_backoff_ms: 1000,
max_backoff_ms: Some(64000),
backoff_multiplier: 2.0,
jitter_ms: 0,
min_jitter_threshold_ms: 0,
recognize_server_retry_delay: false,
max_server_retry_delay_ms: None,
max_retry_exception_elapsed_time_ms: Some(
DEFAULT_MAX_RETRY_EXCEPTION_ELAPSED_TIME * 1000,
),
}
}
}
impl RetryPolicy {
pub fn no_retries() -> Self {
Self {
max_retries: Some(0),
..Self::default()
}
}
pub fn can_retry(&self, status: &tonic::Status) -> bool {
match status.code() {
tonic::Code::Unavailable => true,
tonic::Code::Internal => status.message().contains("INVALID_CURSOR.DISCONNECTED"),
_ => false,
}
}
}
pub struct RetryPolicyState {
policy: RetryPolicy,
attempt: u32,
next_wait_ms: f64,
started: std::time::Instant,
}
impl RetryPolicyState {
pub fn new(policy: RetryPolicy) -> Self {
Self {
next_wait_ms: policy.initial_backoff_ms as f64,
policy,
attempt: 0,
started: std::time::Instant::now(),
}
}
pub fn next_attempt(&mut self, server_retry_delay_ms: Option<u64>) -> Option<u64> {
if let Some(max_ms) = self.policy.max_retry_exception_elapsed_time_ms {
if self.started.elapsed().as_millis() as u64 >= max_ms {
return None;
}
}
if let Some(max) = self.policy.max_retries {
if self.attempt >= max {
return None;
}
}
self.attempt += 1;
let mut wait_time = self.next_wait_ms;
if let Some(max_backoff) = self.policy.max_backoff_ms {
self.next_wait_ms = f64::min(
max_backoff as f64,
wait_time * self.policy.backoff_multiplier,
);
} else {
self.next_wait_ms = wait_time * self.policy.backoff_multiplier;
}
if self.policy.recognize_server_retry_delay {
if let Some(delay) = server_retry_delay_ms {
let max_delay = self.policy.max_server_retry_delay_ms.unwrap_or(delay);
let delay = u64::min(delay, max_delay);
wait_time = f64::max(wait_time, delay as f64);
}
}
if wait_time >= self.policy.min_jitter_threshold_ms as f64 {
wait_time += rand_jitter(self.policy.jitter_ms);
}
Some(wait_time.ceil() as u64)
}
pub fn policy(&self) -> &RetryPolicy {
&self.policy
}
pub fn attempt(&self) -> u32 {
self.attempt
}
}
fn rand_jitter(max: u64) -> f64 {
if max == 0 {
return 0.0;
}
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
| 1;
let mut x = seed;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
let frac = (x >> 11) as f64 / (1u64 << 53) as f64; frac * (max as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retry_policy_backoff_sequence() {
let policy = RetryPolicy {
max_retries: Some(5),
initial_backoff_ms: 100,
max_backoff_ms: Some(1000),
backoff_multiplier: 2.0,
jitter_ms: 0,
min_jitter_threshold_ms: 0,
recognize_server_retry_delay: false,
max_server_retry_delay_ms: None,
max_retry_exception_elapsed_time_ms: None,
};
let mut state = RetryPolicyState::new(policy);
assert_eq!(state.next_attempt(None), Some(100));
assert_eq!(state.next_attempt(None), Some(200));
assert_eq!(state.next_attempt(None), Some(400));
assert_eq!(state.next_attempt(None), Some(800));
assert_eq!(state.next_attempt(None), Some(1000));
assert_eq!(state.next_attempt(None), None);
}
#[test]
fn test_can_retry_classification() {
let p = RetryPolicy::default();
assert!(p.can_retry(&tonic::Status::unavailable("server restarting")));
assert!(p.can_retry(&tonic::Status::internal(
"INVALID_CURSOR.DISCONNECTED: stream dropped"
)));
assert!(!p.can_retry(&tonic::Status::internal("some other internal error")));
assert!(!p.can_retry(&tonic::Status::not_found("missing")));
assert!(!p.can_retry(&tonic::Status::invalid_argument("bad")));
}
#[test]
fn test_jitter_within_bounds() {
for _ in 0..50 {
let j = rand_jitter(100);
assert!((0.0..100.0).contains(&j), "jitter {j} out of range");
}
assert_eq!(rand_jitter(0), 0.0);
}
#[test]
fn test_no_retries_policy_allows_a_single_attempt() {
let policy = RetryPolicy::no_retries();
assert_eq!(policy.max_retries, Some(0));
let mut state = RetryPolicyState::new(policy);
assert_eq!(state.next_attempt(None), None);
}
#[test]
fn test_next_attempt_adds_jitter_above_threshold() {
let policy = RetryPolicy {
max_retries: Some(3),
initial_backoff_ms: 100,
max_backoff_ms: Some(1000),
backoff_multiplier: 2.0,
jitter_ms: 40,
min_jitter_threshold_ms: 10,
recognize_server_retry_delay: false,
max_server_retry_delay_ms: None,
max_retry_exception_elapsed_time_ms: None,
};
let mut state = RetryPolicyState::new(policy);
let w = state.next_attempt(None).expect("first retry allowed");
assert!(
(100..=140).contains(&w),
"expected 100..=140 with jitter, got {w}"
);
assert_eq!(state.attempt(), 1);
}
#[test]
fn test_next_attempt_honors_server_retry_delay() {
let policy = RetryPolicy {
max_retries: Some(3),
initial_backoff_ms: 100,
max_backoff_ms: Some(10_000),
backoff_multiplier: 2.0,
jitter_ms: 0,
min_jitter_threshold_ms: 0,
recognize_server_retry_delay: true,
max_server_retry_delay_ms: Some(5_000),
max_retry_exception_elapsed_time_ms: None,
};
let mut state = RetryPolicyState::new(policy);
assert_eq!(state.next_attempt(Some(2_000)), Some(2_000));
let w = state
.next_attempt(Some(9_999))
.expect("second retry allowed");
assert!(
w <= 5_000,
"server delay must be clamped to the cap, got {w}"
);
}
#[test]
fn test_retry_policy_respects_max_retries() {
let policy = RetryPolicy {
max_retries: Some(2),
initial_backoff_ms: 50,
max_backoff_ms: None,
backoff_multiplier: 1.0,
jitter_ms: 0,
min_jitter_threshold_ms: 0,
recognize_server_retry_delay: false,
max_server_retry_delay_ms: None,
max_retry_exception_elapsed_time_ms: None,
};
let mut state = RetryPolicyState::new(policy);
assert_eq!(state.next_attempt(None), Some(50));
assert_eq!(state.next_attempt(None), Some(50));
assert_eq!(state.next_attempt(None), None);
}
#[test]
fn test_retry_policy_no_max_retries() {
let policy = RetryPolicy {
max_retries: None,
initial_backoff_ms: 10,
max_backoff_ms: Some(100),
backoff_multiplier: 2.0,
jitter_ms: 0,
min_jitter_threshold_ms: 0,
recognize_server_retry_delay: false,
max_server_retry_delay_ms: None,
max_retry_exception_elapsed_time_ms: None,
};
let mut state = RetryPolicyState::new(policy);
for _ in 0..10 {
assert!(state.next_attempt(None).is_some());
}
}
#[test]
fn test_server_retry_delay_recognized() {
let policy = RetryPolicy {
max_retries: Some(3),
initial_backoff_ms: 100,
max_backoff_ms: Some(1000),
backoff_multiplier: 2.0,
jitter_ms: 0,
min_jitter_threshold_ms: 0,
recognize_server_retry_delay: true,
max_server_retry_delay_ms: Some(500),
max_retry_exception_elapsed_time_ms: None,
};
let mut state = RetryPolicyState::new(policy);
let wait = state.next_attempt(Some(250));
assert_eq!(wait, Some(250));
let wait = state.next_attempt(Some(50));
assert_eq!(wait, Some(200)); }
#[test]
fn test_attempt_count() {
let policy = RetryPolicy {
max_retries: Some(3),
initial_backoff_ms: 50,
max_backoff_ms: None,
backoff_multiplier: 1.0,
jitter_ms: 0,
min_jitter_threshold_ms: 0,
recognize_server_retry_delay: false,
max_server_retry_delay_ms: None,
max_retry_exception_elapsed_time_ms: None,
};
let mut state = RetryPolicyState::new(policy);
assert_eq!(state.attempt(), 0);
state.next_attempt(None);
assert_eq!(state.attempt(), 1);
state.next_attempt(None);
assert_eq!(state.attempt(), 2);
}
#[test]
fn test_elapsed_time_ceiling_exceeded() {
let policy = RetryPolicy {
max_retries: Some(10),
initial_backoff_ms: 50,
max_backoff_ms: None,
backoff_multiplier: 1.0,
jitter_ms: 0,
min_jitter_threshold_ms: 0,
recognize_server_retry_delay: false,
max_server_retry_delay_ms: None,
max_retry_exception_elapsed_time_ms: Some(0),
};
let mut state = RetryPolicyState::new(policy);
assert_eq!(state.next_attempt(None), None);
}
#[test]
fn test_elapsed_time_ceiling_with_retries_allowed() {
let policy = RetryPolicy {
max_retries: Some(2),
initial_backoff_ms: 50,
max_backoff_ms: None,
backoff_multiplier: 1.0,
jitter_ms: 0,
min_jitter_threshold_ms: 0,
recognize_server_retry_delay: false,
max_server_retry_delay_ms: None,
max_retry_exception_elapsed_time_ms: Some(u64::MAX),
};
let mut state = RetryPolicyState::new(policy);
assert_eq!(state.next_attempt(None), Some(50));
assert_eq!(state.next_attempt(None), Some(50));
assert_eq!(state.next_attempt(None), None);
}
}