1use pe_core::node::NodeResult;
7use pe_core::state::StateUpdate;
8use serde::{Deserialize, Serialize};
9use std::future::Future;
10use std::time::Duration;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct RetryPolicy {
25 pub max_attempts: u32,
27 pub initial_interval: Duration,
29 pub backoff_factor: f64,
31 pub max_interval: Duration,
33 pub jitter: bool,
35}
36
37impl Default for RetryPolicy {
38 fn default() -> Self {
39 Self {
40 max_attempts: 3,
41 initial_interval: Duration::from_millis(200),
42 backoff_factor: 2.0,
43 max_interval: Duration::from_secs(10),
44 jitter: true,
45 }
46 }
47}
48
49pub async fn with_retry<F, Fut, U>(policy: &RetryPolicy, f: F) -> NodeResult<U>
65where
66 F: Fn() -> Fut,
67 Fut: Future<Output = NodeResult<U>>,
68 U: StateUpdate,
69{
70 let mut attempts = 0u32;
71 let mut delay = policy.initial_interval;
72
73 loop {
74 let result = f().await;
75
76 match &result {
77 NodeResult::Error(e) if e.is_retryable() && attempts < policy.max_attempts => {
78 attempts += 1;
79 let sleep_dur = if policy.jitter {
80 apply_jitter(delay)
81 } else {
82 delay
83 };
84 tokio::time::sleep(sleep_dur).await;
85 delay = next_delay(delay, policy.backoff_factor, policy.max_interval);
86 }
87 _ => return result,
88 }
89 }
90}
91
92pub(crate) fn next_delay(current: Duration, factor: f64, max: Duration) -> Duration {
94 let next = current.mul_f64(factor);
95 if next > max { max } else { next }
96}
97
98pub(crate) fn apply_jitter(delay: Duration) -> Duration {
100 let nanos = delay.as_nanos() as u64;
101 let jitter_nanos = pseudo_random_u64(nanos.max(1));
102 let half = nanos / 2;
103 Duration::from_nanos(half + jitter_nanos)
104}
105
106fn pseudo_random_u64(range: u64) -> u64 {
111 if range == 0 {
112 return 0;
113 }
114 use std::collections::hash_map::RandomState;
115 use std::hash::{BuildHasher, Hasher};
116 let mut hasher = RandomState::new().build_hasher();
117 hasher.write_u64(range);
118 hasher.finish() % range
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use pe_core::error::PeError;
125 use pe_core::node::NodeResult;
126 use std::sync::atomic::{AtomicU32, Ordering};
127
128 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
129 struct DummyUpdate;
130 impl pe_core::state::StateUpdate for DummyUpdate {}
131
132 #[tokio::test]
133 async fn test_succeeds_on_first_try() {
134 let policy = RetryPolicy {
135 max_attempts: 3,
136 jitter: false,
137 ..Default::default()
138 };
139 let result = with_retry(&policy, || async {
140 NodeResult::<DummyUpdate>::Update(DummyUpdate)
141 })
142 .await;
143 assert!(matches!(result, NodeResult::Update(_)));
144 }
145
146 #[tokio::test]
147 async fn test_retries_then_succeeds() {
148 let attempts = AtomicU32::new(0);
149 let policy = RetryPolicy {
150 max_attempts: 3,
151 initial_interval: Duration::from_millis(1),
152 jitter: false,
153 ..Default::default()
154 };
155
156 let result = with_retry(&policy, || {
157 let count = attempts.fetch_add(1, Ordering::SeqCst);
158 async move {
159 if count < 2 {
160 NodeResult::<DummyUpdate>::Error(PeError::Timeout { seconds: 1.0 })
161 } else {
162 NodeResult::Update(DummyUpdate)
163 }
164 }
165 })
166 .await;
167
168 assert!(matches!(result, NodeResult::Update(_)));
169 assert_eq!(attempts.load(Ordering::SeqCst), 3); }
171
172 #[tokio::test]
173 async fn test_exhausts_retries_returns_error() {
174 let attempts = AtomicU32::new(0);
175 let policy = RetryPolicy {
176 max_attempts: 2,
177 initial_interval: Duration::from_millis(1),
178 jitter: false,
179 ..Default::default()
180 };
181
182 let result = with_retry(&policy, || {
183 attempts.fetch_add(1, Ordering::SeqCst);
184 async { NodeResult::<DummyUpdate>::Error(PeError::Timeout { seconds: 1.0 }) }
185 })
186 .await;
187
188 assert!(matches!(result, NodeResult::Error(PeError::Timeout { .. })));
189 assert_eq!(attempts.load(Ordering::SeqCst), 3);
191 }
192
193 #[tokio::test]
194 async fn test_non_retryable_error_not_retried() {
195 let attempts = AtomicU32::new(0);
196 let policy = RetryPolicy {
197 max_attempts: 3,
198 initial_interval: Duration::from_millis(1),
199 jitter: false,
200 ..Default::default()
201 };
202
203 let result = with_retry(&policy, || {
204 attempts.fetch_add(1, Ordering::SeqCst);
205 async {
206 NodeResult::<DummyUpdate>::Error(PeError::PermissionDenied {
207 action: "test".into(),
208 })
209 }
210 })
211 .await;
212
213 assert!(matches!(
214 result,
215 NodeResult::Error(PeError::PermissionDenied { .. })
216 ));
217 assert_eq!(attempts.load(Ordering::SeqCst), 1); }
219
220 #[tokio::test]
221 async fn test_interrupt_not_retried() {
222 let policy = RetryPolicy {
223 max_attempts: 3,
224 jitter: false,
225 ..Default::default()
226 };
227
228 let result = with_retry(&policy, || async {
229 NodeResult::<DummyUpdate>::Interrupt(pe_core::node::InterruptRequest {
230 reason: "test".into(),
231 partial_update: None,
232 resume_point: "test:0".into(),
233 })
234 })
235 .await;
236
237 assert!(matches!(result, NodeResult::Interrupt(_)));
238 }
239
240 #[test]
241 fn test_next_delay_exponential() {
242 let d = next_delay(Duration::from_millis(100), 2.0, Duration::from_secs(10));
243 assert_eq!(d, Duration::from_millis(200));
244 }
245
246 #[test]
247 fn test_next_delay_capped_at_max() {
248 let d = next_delay(Duration::from_secs(8), 2.0, Duration::from_secs(10));
249 assert_eq!(d, Duration::from_secs(10));
250 }
251}