openkind_client/retry/
policy.rs1use std::time::Duration;
2
3use crate::error::Error;
4
5pub const DEFAULT_RETRY_STATUSES: [u16; 2] = [408, 429];
8
9pub type RetryPredicate = std::sync::Arc<dyn Fn(&Error) -> bool + Send + Sync>;
13
14#[derive(Clone)]
28pub struct RetryPolicy {
29 pub(crate) max_retries: u32,
30 pub(crate) backoff_initial: Duration,
31 pub(crate) backoff_max: Duration,
32 pub(crate) backoff_jitter: f64,
33 pub(crate) retry_statuses: Vec<u16>,
34 pub(crate) retry_server_errors: bool,
35 pub(crate) respect_retry_after: bool,
36 pub(crate) retry_connection_errors: bool,
37 pub(crate) retry_timeout_errors: bool,
38 pub(crate) total_timeout: Option<Duration>,
39 pub(crate) retry_predicate: Option<RetryPredicate>,
40}
41
42impl std::fmt::Debug for RetryPolicy {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("RetryPolicy")
47 .field("max_retries", &self.max_retries)
48 .field("backoff_initial", &self.backoff_initial)
49 .field("backoff_max", &self.backoff_max)
50 .field("backoff_jitter", &self.backoff_jitter)
51 .field("retry_statuses", &self.retry_statuses)
52 .field("retry_server_errors", &self.retry_server_errors)
53 .field("respect_retry_after", &self.respect_retry_after)
54 .field("retry_connection_errors", &self.retry_connection_errors)
55 .field("retry_timeout_errors", &self.retry_timeout_errors)
56 .field("total_timeout", &self.total_timeout)
57 .field("retry_predicate", &self.retry_predicate.is_some())
58 .finish()
59 }
60}
61
62impl Default for RetryPolicy {
63 fn default() -> Self {
64 Self {
65 max_retries: 2,
66 backoff_initial: Duration::from_millis(500),
67 backoff_max: Duration::from_secs(5),
68 backoff_jitter: 0.25,
69 retry_statuses: DEFAULT_RETRY_STATUSES.to_vec(),
70 retry_server_errors: true,
71 respect_retry_after: true,
72 retry_connection_errors: true,
73 retry_timeout_errors: true,
74 total_timeout: Some(Duration::from_secs(30)),
75 retry_predicate: None,
76 }
77 }
78}
79
80impl RetryPolicy {
81 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn max_retries(mut self, max_retries: u32) -> Self {
88 self.max_retries = max_retries;
89 self
90 }
91
92 pub fn backoff_initial(mut self, initial: Duration) -> Self {
95 self.backoff_initial = initial;
96 self
97 }
98
99 pub fn backoff_max(mut self, max: Duration) -> Self {
101 self.backoff_max = max;
102 self
103 }
104
105 pub fn backoff_jitter(mut self, jitter: f64) -> Self {
108 self.backoff_jitter = jitter;
109 self
110 }
111
112 pub fn retry_status(mut self, status: u16) -> Self {
116 if !self.retry_statuses.contains(&status) {
117 self.retry_statuses.push(status);
118 }
119 self
120 }
121
122 pub fn with_retry_statuses(mut self, statuses: impl IntoIterator<Item = u16>) -> Self {
129 self.retry_statuses = statuses.into_iter().collect();
130 self
131 }
132
133 pub fn retry_predicate(mut self, predicate: Option<RetryPredicate>) -> Self {
138 self.retry_predicate = predicate;
139 self
140 }
141
142 pub fn retry_server_errors(mut self, retry: bool) -> Self {
144 self.retry_server_errors = retry;
145 self
146 }
147
148 pub fn respect_retry_after(mut self, respect: bool) -> Self {
151 self.respect_retry_after = respect;
152 self
153 }
154
155 pub fn retry_connection_errors(mut self, retry: bool) -> Self {
157 self.retry_connection_errors = retry;
158 self
159 }
160
161 pub fn retry_timeout_errors(mut self, retry: bool) -> Self {
163 self.retry_timeout_errors = retry;
164 self
165 }
166
167 pub fn total_timeout(mut self, timeout: Option<Duration>) -> Self {
171 self.total_timeout = timeout;
172 self
173 }
174
175 pub(crate) fn validate(&self) -> Result<(), Error> {
178 if !(0.0..=1.0).contains(&self.backoff_jitter) || !self.backoff_jitter.is_finite() {
179 return Err(Error::Config(format!(
180 "backoff_jitter must be between 0.0 and 1.0, got {}",
181 self.backoff_jitter
182 )));
183 }
184 Ok(())
185 }
186
187 pub fn is_retryable(&self, error: &Error) -> bool {
189 let builtin = match error {
190 Error::Connection(_) => self.retry_connection_errors,
191 Error::Timeout { .. } => self.retry_timeout_errors,
192 Error::Api(api) => {
193 self.retry_server_errors && (500..600).contains(&api.status)
194 || self.retry_statuses.contains(&api.status)
195 }
196 Error::Config(_)
198 | Error::Decode { .. }
199 | Error::InvalidResponse { .. }
200 | Error::ResponseTooLarge { .. } => false,
201 };
202 builtin || self.retry_predicate.as_ref().is_some_and(|f| f(error))
203 }
204
205 pub fn delay_for(&self, error: &Error, retry_index: u32) -> Duration {
211 if self.respect_retry_after {
212 if let Some(delay) = error.retry_after() {
213 return delay;
214 }
215 }
216 self.backoff_delay(retry_index)
217 }
218
219 pub fn backoff_delay(&self, retry_index: u32) -> Duration {
225 if self.backoff_initial.is_zero() || self.backoff_max.is_zero() {
226 return Duration::ZERO;
227 }
228 let initial_ns = u64::try_from(self.backoff_initial.as_nanos()).unwrap_or(u64::MAX);
229 let multiplier = 1u64.checked_shl(retry_index).unwrap_or(u64::MAX);
230 let exponential = initial_ns.saturating_mul(multiplier);
231 let maximum_ns = u64::try_from(self.backoff_max.as_nanos()).unwrap_or(u64::MAX);
232 let capped = exponential.min(maximum_ns);
233 let jitter_ns = (capped as f64 * self.backoff_jitter * fastrand::f64()) as u64;
234 Duration::from_nanos(capped.saturating_sub(jitter_ns))
235 }
236}