oci_rust_sdk/core/
retry.rs1use std::time::Duration;
2use tokio::time::sleep;
3
4#[derive(Debug, Clone)]
6pub struct RetryConfiguration {
7 pub max_attempts: u32,
9 pub base_delay: Duration,
11 pub max_delay: Duration,
13}
14
15impl Default for RetryConfiguration {
16 fn default() -> Self {
17 Self {
19 max_attempts: 8,
20 base_delay: Duration::from_secs(1),
21 max_delay: Duration::from_secs(30),
22 }
23 }
24}
25
26pub struct Retrier {
28 config: RetryConfiguration,
29}
30
31impl Retrier {
32 pub fn new() -> Self {
34 Self {
35 config: RetryConfiguration::default(),
36 }
37 }
38
39 pub fn with_config(config: RetryConfiguration) -> Self {
41 Self { config }
42 }
43
44 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 fn calculate_backoff(&self, attempt: u32) -> Duration {
67 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 assert_eq!(retrier.calculate_backoff(1), Duration::from_secs(1));
98
99 assert_eq!(retrier.calculate_backoff(2), Duration::from_secs(2));
101
102 assert_eq!(retrier.calculate_backoff(3), Duration::from_secs(4));
104
105 assert_eq!(retrier.calculate_backoff(4), Duration::from_secs(8));
107
108 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 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 assert_eq!(call_count, 1);
175 }
176}