chainrpc_core/policy/
retry.rs1use std::time::Duration;
4
5#[derive(Debug, Clone)]
7pub struct RetryConfig {
8 pub max_retries: u32,
10 pub initial_backoff: Duration,
12 pub max_backoff: Duration,
14 pub multiplier: f64,
16 pub jitter_fraction: f64,
18}
19
20impl Default for RetryConfig {
21 fn default() -> Self {
22 Self {
23 max_retries: 3,
24 initial_backoff: Duration::from_millis(100),
25 max_backoff: Duration::from_secs(10),
26 multiplier: 2.0,
27 jitter_fraction: 0.1,
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct RetryPolicy {
35 pub config: RetryConfig,
36}
37
38impl RetryPolicy {
39 pub fn new(config: RetryConfig) -> Self {
40 Self { config }
41 }
42
43 pub fn next_delay(&self, attempt: u32) -> Option<Duration> {
46 if attempt > self.config.max_retries {
47 return None;
48 }
49 let base_ms = self.config.initial_backoff.as_millis() as f64
50 * self.config.multiplier.powi((attempt - 1) as i32);
51 let cap_ms = self.config.max_backoff.as_millis() as f64;
52 let capped = base_ms.min(cap_ms);
53
54 let jitter_ms = if self.config.jitter_fraction > 0.0 {
56 let nanos = std::time::SystemTime::now()
57 .duration_since(std::time::UNIX_EPOCH)
58 .unwrap_or_default()
59 .subsec_nanos();
60 let random_factor = (nanos as f64 / u32::MAX as f64) * 2.0 - 1.0;
62 capped * self.config.jitter_fraction * random_factor
63 } else {
64 0.0
65 };
66
67 let total_ms = ((capped + jitter_ms).max(1.0)) as u64;
68 Some(Duration::from_millis(total_ms))
69 }
70
71 pub fn should_retry(&self, attempt: u32) -> bool {
73 attempt <= self.config.max_retries
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn first_retry_delay() {
83 let policy = RetryPolicy::new(RetryConfig {
84 max_retries: 3,
85 initial_backoff: Duration::from_millis(100),
86 max_backoff: Duration::from_secs(30),
87 multiplier: 2.0,
88 jitter_fraction: 0.0,
89 });
90 let d1 = policy.next_delay(1).unwrap();
91 let d2 = policy.next_delay(2).unwrap();
92 let d3 = policy.next_delay(3).unwrap();
93 assert_eq!(d1.as_millis(), 100);
94 assert_eq!(d2.as_millis(), 200);
95 assert_eq!(d3.as_millis(), 400);
96 assert!(policy.next_delay(4).is_none());
97 }
98
99 #[test]
100 fn delay_capped_at_max() {
101 let policy = RetryPolicy::new(RetryConfig {
102 max_retries: 10,
103 initial_backoff: Duration::from_millis(100),
104 max_backoff: Duration::from_millis(500),
105 multiplier: 10.0,
106 jitter_fraction: 0.0,
107 });
108 let d5 = policy.next_delay(5).unwrap();
110 assert!(d5 <= Duration::from_millis(500), "d5={d5:?} exceeds max");
111 }
112
113 #[test]
114 fn should_retry_boundary() {
115 let policy = RetryPolicy::new(RetryConfig {
116 max_retries: 2,
117 ..Default::default()
118 });
119 assert!(policy.should_retry(1));
120 assert!(policy.should_retry(2));
121 assert!(!policy.should_retry(3));
122 }
123}