salesforce_client/
retry.rs1use crate::error::{SfError, SfResult};
6use std::time::Duration;
8use tracing::{debug, warn};
9
10#[derive(Debug, Clone)]
12pub struct RetryConfig {
13 pub max_retries: u32,
15
16 pub initial_interval: Duration,
18
19 pub max_interval: Duration,
21
22 pub multiplier: f64,
24
25 pub max_elapsed_time: Option<Duration>,
27}
28
29impl Default for RetryConfig {
30 fn default() -> Self {
31 Self {
32 max_retries: 3,
33 initial_interval: Duration::from_millis(500),
34 max_interval: Duration::from_secs(30),
35 multiplier: 2.0,
36 max_elapsed_time: Some(Duration::from_secs(300)), }
38 }
39}
40
41impl RetryConfig {
42 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn max_retries(mut self, max: u32) -> Self {
49 self.max_retries = max;
50 self
51 }
52
53 pub fn initial_interval(mut self, duration: Duration) -> Self {
55 self.initial_interval = duration;
56 self
57 }
58
59 pub fn max_interval(mut self, duration: Duration) -> Self {
61 self.max_interval = duration;
62 self
63 }
64
65 pub fn no_retry() -> Self {
67 Self {
68 max_retries: 0,
69 ..Default::default()
70 }
71 }
72}
73
74pub(crate) fn is_retryable(error: &SfError) -> bool {
76 match error {
77 SfError::Network(_) => true,
79
80 SfError::RateLimit { .. } => true,
82
83 SfError::Timeout { .. } => true,
85
86 SfError::Api { status, .. } => {
88 matches!(
89 *status,
90 408 |
92 429 |
94 500 |
96 502 |
98 503 |
100 504
102 )
103 }
104
105 _ => false,
107 }
108}
109
110pub async fn with_retry<F, Fut, T>(config: &RetryConfig, operation: F) -> SfResult<T>
119where
120 F: Fn() -> Fut,
121 Fut: std::future::Future<Output = SfResult<T>>,
122{
123 if config.max_retries == 0 {
124 return operation().await;
126 }
127
128 let mut attempt = 0;
129 let mut delay = config.initial_interval;
130
131 loop {
132 attempt += 1;
133
134 match operation().await {
135 Ok(result) => {
136 if attempt > 1 {
137 debug!("Operation succeeded after {} attempts", attempt);
138 }
139 return Ok(result);
140 }
141 Err(e) => {
142 if is_retryable(&e) && attempt <= config.max_retries {
143 warn!(
144 "Attempt {} failed: {}. Retrying in {:?}...",
145 attempt, e, delay
146 );
147 tokio::time::sleep(delay).await;
148
149 delay = Duration::min(
151 Duration::from_secs_f64(delay.as_secs_f64() * config.multiplier),
152 config.max_interval,
153 );
154 } else {
155 if attempt > config.max_retries {
156 warn!("Max retries ({}) exceeded. Giving up.", config.max_retries);
157 } else {
158 debug!("Error is not retryable: {}", e);
159 }
160 return Err(e);
161 }
162 }
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_retry_config_builder() {
173 let config = RetryConfig::new()
174 .max_retries(5)
175 .initial_interval(Duration::from_millis(100));
176
177 assert_eq!(config.max_retries, 5);
178 assert_eq!(config.initial_interval, Duration::from_millis(100));
179 }
180
181 #[test]
182 fn test_is_retryable() {
183 assert!(is_retryable(&SfError::RateLimit { retry_after: None }));
185 assert!(is_retryable(&SfError::Timeout { seconds: 30 }));
186 assert!(is_retryable(&SfError::Api {
187 status: 503,
188 body: "Service Unavailable".to_string()
189 }));
190
191 assert!(!is_retryable(&SfError::Auth("Invalid token".to_string())));
193 assert!(!is_retryable(&SfError::NotFound {
194 sobject: "Account".to_string(),
195 id: "123".to_string()
196 }));
197 assert!(!is_retryable(&SfError::Api {
198 status: 400,
199 body: "Bad Request".to_string()
200 }));
201 }
202
203 #[tokio::test]
204 async fn test_with_retry_success() {
205 let config = RetryConfig::no_retry();
206
207 let result = with_retry(&config, || async { Ok::<i32, SfError>(42) }).await;
208
209 assert!(result.is_ok());
210 assert_eq!(result.unwrap(), 42);
211 }
212
213 #[tokio::test]
214 async fn test_with_retry_non_retryable_error() {
215 let config = RetryConfig::new().max_retries(3);
216
217 let result = with_retry(&config, || async {
218 Err::<i32, SfError>(SfError::Auth("Invalid token".to_string()))
219 })
220 .await;
221
222 assert!(result.is_err());
223 }
225}