connpool/resilience/
retry.rs1use std::error::Error;
4use std::sync::Arc;
5
6use crate::core::errors::PoolError;
7use crate::rng::Rng;
8
9type RetryOn = Arc<dyn Fn(&(dyn Error + 'static)) -> bool + Send + Sync>;
11
12#[derive(Clone)]
18pub struct RetryPolicy {
19 pub max_attempts: u32,
21 pub base_delay: f64,
23 pub max_delay: f64,
25 pub multiplier: f64,
27 pub jitter: f64,
29 pub retry_on: Option<RetryOn>,
31}
32
33impl std::fmt::Debug for RetryPolicy {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 f.debug_struct("RetryPolicy")
36 .field("max_attempts", &self.max_attempts)
37 .field("base_delay", &self.base_delay)
38 .field("max_delay", &self.max_delay)
39 .field("multiplier", &self.multiplier)
40 .field("jitter", &self.jitter)
41 .field("retry_on", &self.retry_on.as_ref().map(|_| "<fn>"))
42 .finish()
43 }
44}
45
46impl Default for RetryPolicy {
47 fn default() -> Self {
48 Self {
49 max_attempts: 3,
50 base_delay: 0.1,
51 max_delay: 5.0,
52 multiplier: 2.0,
53 jitter: 0.5,
54 retry_on: None,
55 }
56 }
57}
58
59impl RetryPolicy {
60 pub fn no_retry() -> Self {
62 Self {
63 max_attempts: 1,
64 ..Self::default()
65 }
66 }
67
68 pub fn with_retry_on<F>(mut self, predicate: F) -> Self
70 where
71 F: Fn(&(dyn Error + 'static)) -> bool + Send + Sync + 'static,
72 {
73 self.retry_on = Some(Arc::new(predicate));
74 self
75 }
76
77 pub fn validate(&self) -> Result<(), PoolError> {
79 if self.max_attempts < 1 {
80 return Err(PoolError::Config("retry.max_attempts must be >= 1".into()));
81 }
82 if self.base_delay < 0.0 || self.max_delay < 0.0 || self.multiplier < 1.0 {
83 return Err(PoolError::Config(
84 "retry delays must be >= 0 and multiplier >= 1".into(),
85 ));
86 }
87 if self.jitter < 0.0 || self.jitter > 1.0 {
88 return Err(PoolError::Config(
89 "retry.jitter must be within [0, 1]".into(),
90 ));
91 }
92 Ok(())
93 }
94
95 pub fn should_retry(&self, err: &(dyn Error + 'static)) -> bool {
97 match &self.retry_on {
98 None => true,
99 Some(p) => p(err),
100 }
101 }
102
103 pub fn delay_for(&self, failed_attempts: u32, rng: &mut dyn Rng) -> f64 {
106 let ceiling = if self.base_delay == 0.0 {
109 0.0
110 } else {
111 self.max_delay
112 .min(self.base_delay * self.multiplier.powi(failed_attempts as i32 - 1))
113 };
114 ceiling * (1.0 - self.jitter + self.jitter * rng.next_f64())
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::rng::SystemRng;
122
123 #[test]
124 fn defaults_and_no_retry() {
125 let p = RetryPolicy::default();
126 assert_eq!(p.max_attempts, 3);
127 assert_eq!(p.base_delay, 0.1);
128 assert_eq!(p.max_delay, 5.0);
129 assert_eq!(p.multiplier, 2.0);
130 assert_eq!(p.jitter, 0.5);
131 assert!(p.retry_on.is_none());
132 assert_eq!(RetryPolicy::no_retry().max_attempts, 1);
133 assert!(format!("{p:?}").contains("RetryPolicy"));
134 assert!(format!("{:?}", p.clone().with_retry_on(|_| true)).contains("<fn>"));
135 }
136
137 #[test]
138 fn validate_rules() {
139 assert!(RetryPolicy::default().validate().is_ok());
140 let bad = RetryPolicy {
141 max_attempts: 0,
142 ..Default::default()
143 };
144 assert!(matches!(bad.validate(), Err(PoolError::Config(_))));
145 let bad = RetryPolicy {
146 base_delay: -1.0,
147 ..Default::default()
148 };
149 assert!(bad.validate().is_err());
150 let bad = RetryPolicy {
151 multiplier: 0.5,
152 ..Default::default()
153 };
154 assert!(bad.validate().is_err());
155 let bad = RetryPolicy {
156 jitter: 1.5,
157 ..Default::default()
158 };
159 assert!(bad.validate().is_err());
160 }
161
162 #[test]
163 fn should_retry_predicate() {
164 let p = RetryPolicy::default();
165 let err: Box<dyn Error + Send + Sync> = "x".into();
166 assert!(p.should_retry(&*err));
167 let p = RetryPolicy::default().with_retry_on(|e| e.to_string() == "retry");
168 let yes: Box<dyn Error + Send + Sync> = "retry".into();
169 let no: Box<dyn Error + Send + Sync> = "stop".into();
170 assert!(p.should_retry(&*yes));
171 assert!(!p.should_retry(&*no));
172 }
173
174 #[test]
175 fn delay_backoff_and_jitter_bounds() {
176 let mut rng = SystemRng::with_seed(7);
177 let p = RetryPolicy::default();
178 for _ in 0..1000 {
180 let d = p.delay_for(1, &mut rng);
181 assert!((0.05..=0.1).contains(&d), "delay {d}");
182 }
183 let d = p.delay_for(2, &mut rng);
185 assert!((0.1..=0.2).contains(&d));
186 let capped = RetryPolicy {
188 base_delay: 10.0,
189 max_delay: 1.0,
190 jitter: 0.0,
191 ..Default::default()
192 };
193 assert_eq!(capped.delay_for(5, &mut rng), 1.0);
194 let det = RetryPolicy {
196 jitter: 0.0,
197 ..Default::default()
198 };
199 assert_eq!(det.delay_for(1, &mut rng), 0.1);
200 }
201
202 #[test]
203 fn delay_zero_base_never_nan_even_on_power_overflow() {
204 let mut rng = SystemRng::with_seed(7);
205 let p = RetryPolicy {
208 base_delay: 0.0,
209 multiplier: 2.0,
210 ..Default::default()
211 };
212 let d = p.delay_for(10_000, &mut rng);
213 assert!(d.is_finite(), "delay {d}");
214 assert_eq!(d, 0.0);
215 }
216}