Skip to main content

jules_api/retry/
mod.rs

1//! Retry and Rate-Limiting handling.
2
3use jules_core::errors::SDKError;
4
5/// Represents a strategy for retrying failed requests.
6pub trait RetryPolicy: Send + Sync {
7    /// Determines whether a request should be retried based on the error.
8    /// If it should be retried, returns the delay in milliseconds.
9    fn should_retry(&self, attempt: u32, error: &SDKError) -> Option<u64>;
10}
11
12/// A simple backoff retry policy.
13#[derive(Debug, Clone)]
14pub struct ExponentialBackoff {
15    /// Maximum number of retries.
16    pub max_retries: u32,
17    /// Base delay in milliseconds.
18    pub base_delay_ms: u64,
19    /// Maximum delay in milliseconds.
20    pub max_delay_ms: u64,
21}
22
23impl Default for ExponentialBackoff {
24    fn default() -> Self {
25        Self {
26            max_retries: 3,
27            base_delay_ms: 100,
28            max_delay_ms: 10_000,
29        }
30    }
31}
32
33impl RetryPolicy for ExponentialBackoff {
34    fn should_retry(&self, attempt: u32, error: &SDKError) -> Option<u64> {
35        if attempt >= self.max_retries {
36            return None;
37        }
38
39        let is_retryable = match error {
40            SDKError::Api(api_err) => {
41                // Retry on rate limits (429) and server errors (5xx)
42                if let Some(status) = api_err.status_code {
43                    status == 429 || (500..=599).contains(&status)
44                } else {
45                    false
46                }
47            }
48            SDKError::Network(_) => true,
49            _ => false,
50        };
51
52        if is_retryable {
53            // Calculate backoff: base_delay * 2^attempt
54            // Attempt is 0-indexed here
55            let mut delay = self.base_delay_ms.saturating_mul(1 << attempt);
56            if delay > self.max_delay_ms {
57                delay = self.max_delay_ms;
58            }
59            Some(delay)
60        } else {
61            None
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use jules_core::errors::{ApiError, NetworkError};
70
71    #[test]
72    fn test_exponential_backoff_max_retries() {
73        let policy = ExponentialBackoff {
74            max_retries: 2,
75            ..Default::default()
76        };
77
78        let error = SDKError::Network(NetworkError::new("Timeout"));
79
80        // Attempt 0
81        assert_eq!(policy.should_retry(0, &error), Some(100));
82        // Attempt 1
83        assert_eq!(policy.should_retry(1, &error), Some(200));
84        // Attempt 2 (max retries reached)
85        assert_eq!(policy.should_retry(2, &error), None);
86    }
87
88    #[test]
89    fn test_exponential_backoff_rate_limit() {
90        let policy = ExponentialBackoff::default();
91        let error = SDKError::Api(ApiError::with_status("Rate Limit Exceeded", 429));
92
93        assert_eq!(policy.should_retry(0, &error), Some(100));
94    }
95
96    #[test]
97    fn test_exponential_backoff_server_error() {
98        let policy = ExponentialBackoff::default();
99        let error = SDKError::Api(ApiError::with_status("Internal Server Error", 500));
100
101        assert_eq!(policy.should_retry(0, &error), Some(100));
102    }
103
104    #[test]
105    fn test_exponential_backoff_bad_request() {
106        let policy = ExponentialBackoff::default();
107        let error = SDKError::Api(ApiError::with_status("Bad Request", 400));
108
109        // Should not retry client errors (400)
110        assert_eq!(policy.should_retry(0, &error), None);
111    }
112
113    #[test]
114    fn test_exponential_backoff_no_status_code() {
115        let policy = ExponentialBackoff::default();
116        let error = SDKError::Api(ApiError::new("Unknown Error"));
117
118        assert_eq!(policy.should_retry(0, &error), None);
119    }
120}