Skip to main content

oci_rust_sdk/core/
retry.rs

1use std::time::Duration;
2use tokio::time::sleep;
3
4/// Retry configuration based on TypeScript SDK's OciSdkDefaultRetryConfiguration
5#[derive(Debug, Clone)]
6pub struct RetryConfiguration {
7    /// Maximum number of retry attempts (default: 8)
8    pub max_attempts: u32,
9    /// Base delay between retries (default: 1 second)
10    pub base_delay: Duration,
11    /// Maximum delay between retries (default: 30 seconds)
12    pub max_delay: Duration,
13}
14
15impl Default for RetryConfiguration {
16    fn default() -> Self {
17        // Based on TypeScript SDK's OciSdkDefaultRetryConfiguration
18        Self {
19            max_attempts: 8,
20            base_delay: Duration::from_secs(1),
21            max_delay: Duration::from_secs(30),
22        }
23    }
24}
25
26/// Retrier handles retry logic with exponential backoff
27pub struct Retrier {
28    config: RetryConfiguration,
29}
30
31impl Retrier {
32    /// Create a new retrier with default configuration
33    pub fn new() -> Self {
34        Self {
35            config: RetryConfiguration::default(),
36        }
37    }
38
39    /// Create a new retrier with custom configuration
40    pub fn with_config(config: RetryConfiguration) -> Self {
41        Self { config }
42    }
43
44    /// Execute an async operation with retry logic
45    pub async fn execute_with_retry<F, Fut, T>(&self, mut operation: F) -> crate::core::Result<T>
46    where
47        F: FnMut() -> Fut,
48        Fut: std::future::Future<Output = crate::core::Result<T>>,
49    {
50        let mut attempts = 0;
51        loop {
52            attempts += 1;
53
54            match operation().await {
55                Ok(result) => return Ok(result),
56                Err(err) if err.is_retryable() && attempts < self.config.max_attempts => {
57                    let delay = self.calculate_backoff(attempts);
58                    sleep(delay).await;
59                }
60                Err(err) => return Err(err),
61            }
62        }
63    }
64
65    /// Calculate exponential backoff delay with capped maximum
66    fn calculate_backoff(&self, attempt: u32) -> Duration {
67        // Exponential backoff: base_delay * 2^(attempt - 1)
68        let delay = self.config.base_delay * 2_u32.saturating_pow(attempt - 1);
69        delay.min(self.config.max_delay)
70    }
71}
72
73impl Default for Retrier {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::core::*;
83
84    #[test]
85    fn test_retry_configuration_default() {
86        let config = RetryConfiguration::default();
87        assert_eq!(config.max_attempts, 8);
88        assert_eq!(config.base_delay, Duration::from_secs(1));
89        assert_eq!(config.max_delay, Duration::from_secs(30));
90    }
91
92    #[test]
93    fn test_calculate_backoff() {
94        let retrier = Retrier::new();
95
96        // First attempt: 1s
97        assert_eq!(retrier.calculate_backoff(1), Duration::from_secs(1));
98
99        // Second attempt: 2s
100        assert_eq!(retrier.calculate_backoff(2), Duration::from_secs(2));
101
102        // Third attempt: 4s
103        assert_eq!(retrier.calculate_backoff(3), Duration::from_secs(4));
104
105        // Fourth attempt: 8s
106        assert_eq!(retrier.calculate_backoff(4), Duration::from_secs(8));
107
108        // Large attempt: capped at max_delay (30s)
109        assert_eq!(retrier.calculate_backoff(10), Duration::from_secs(30));
110    }
111
112    #[tokio::test]
113    async fn test_retry_success_on_first_attempt() {
114        let retrier = Retrier::new();
115        let mut call_count = 0;
116
117        let result = retrier
118            .execute_with_retry(|| {
119                call_count += 1;
120                async move { Ok::<i32, OciError>(42) }
121            })
122            .await;
123
124        assert!(result.is_ok());
125        assert_eq!(result.unwrap(), 42);
126        assert_eq!(call_count, 1);
127    }
128
129    #[tokio::test]
130    async fn test_retry_success_after_failures() {
131        let retrier = Retrier::new();
132        let call_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
133
134        let result = retrier
135            .execute_with_retry(|| {
136                let count = call_count.clone();
137                async move {
138                    let current = count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
139                    if current < 3 {
140                        // Use a retryable error (ServiceError with 500 status)
141                        Err(OciError::ServiceError {
142                            status: 500,
143                            code: "InternalError".to_string(),
144                            message: "test error".to_string(),
145                        })
146                    } else {
147                        Ok(42)
148                    }
149                }
150            })
151            .await;
152
153        assert!(result.is_ok());
154        assert_eq!(result.unwrap(), 42);
155        assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 3);
156    }
157
158    #[tokio::test]
159    async fn test_retry_non_retryable_error() {
160        let retrier = Retrier::new();
161        let mut call_count = 0;
162
163        let result = retrier
164            .execute_with_retry(|| {
165                call_count += 1;
166                async move {
167                    Err::<i32, OciError>(OciError::AuthError("Invalid credentials".to_string()))
168                }
169            })
170            .await;
171
172        assert!(result.is_err());
173        // Should not retry non-retryable errors
174        assert_eq!(call_count, 1);
175    }
176}