1use std::pin::Pin;
21use std::sync::Arc;
22use std::time::Duration;
23
24use async_trait::async_trait;
25use futures_util::{Stream, StreamExt};
26
27use super::any::RunnableAny;
28use super::config::RunnableConfig;
29use super::error::LcelError;
30use super::runnable_trait::Runnable;
31
32#[derive(Clone)]
34pub enum RetryOn {
35 AllErrors,
37 TransientErrors,
40 Custom(Arc<dyn Fn(&str) -> bool + Send + Sync>),
42}
43
44impl std::fmt::Debug for RetryOn {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::AllErrors => write!(f, "AllErrors"),
48 Self::TransientErrors => write!(f, "TransientErrors"),
49 Self::Custom(_) => write!(f, "Custom(<closure>)"),
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct RetryConfig {
57 pub max_retries: usize,
59
60 pub initial_delay: Duration,
62
63 pub max_delay: Duration,
65
66 pub backoff_multiplier: f64,
69
70 pub retry_on: RetryOn,
72}
73
74impl Default for RetryConfig {
75 fn default() -> Self {
76 Self {
77 max_retries: 3,
78 initial_delay: Duration::from_millis(500),
79 max_delay: Duration::from_secs(10),
80 backoff_multiplier: 2.0,
81 retry_on: RetryOn::TransientErrors,
82 }
83 }
84}
85
86impl RetryConfig {
87 pub fn new(max_retries: usize) -> Self {
89 Self {
90 max_retries,
91 ..Default::default()
92 }
93 }
94
95 pub fn with_initial_delay(mut self, delay: Duration) -> Self {
97 self.initial_delay = delay;
98 self
99 }
100
101 pub fn with_max_delay(mut self, delay: Duration) -> Self {
103 self.max_delay = delay;
104 self
105 }
106
107 pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
109 self.backoff_multiplier = multiplier;
110 self
111 }
112
113 pub fn with_retry_on(mut self, retry_on: RetryOn) -> Self {
115 self.retry_on = retry_on;
116 self
117 }
118
119 fn should_retry(&self, error: &str) -> bool {
121 match &self.retry_on {
122 RetryOn::AllErrors => true,
123 RetryOn::TransientErrors => is_transient_error(error),
124 RetryOn::Custom(predicate) => predicate(error),
125 }
126 }
127
128 fn delay_for_attempt(&self, attempt: usize) -> Duration {
130 let multiplier = self.backoff_multiplier.powi(attempt as i32);
131 let delay = self.initial_delay.as_secs_f64() * multiplier;
132 let delay = delay.min(self.max_delay.as_secs_f64());
133 Duration::from_secs_f64(delay)
134 }
135}
136
137fn is_transient_error(error: &str) -> bool {
139 let error_lower = error.to_lowercase();
140
141 for code in &["429", "500", "502", "503", "504"] {
143 if error_lower.contains(code) {
144 return true;
145 }
146 }
147
148 let transient_patterns = [
150 "rate limit",
151 "rate_limit",
152 "ratelimit",
153 "too many requests",
154 "timeout",
155 "timed out",
156 "connection reset",
157 "connection refused",
158 "temporary failure",
159 "service unavailable",
160 "internal server error",
161 "overloaded",
162 "capacity",
163 ];
164
165 for pattern in &transient_patterns {
166 if error_lower.contains(pattern) {
167 return true;
168 }
169 }
170
171 false
172}
173
174pub struct RunnableRetry<I: Send + Sync + 'static, O: Send + Sync + 'static> {
176 runnable: Arc<dyn RunnableAny>,
177 retry_config: RetryConfig,
178 _marker: std::marker::PhantomData<(I, O)>,
179}
180
181impl<I: Send + Sync + 'static, O: Send + Sync + 'static> RunnableRetry<I, O> {
182 pub fn new(runnable: Box<dyn RunnableAny>, retry_config: RetryConfig) -> Self {
184 Self {
185 runnable: Arc::from(runnable),
186 retry_config,
187 _marker: std::marker::PhantomData,
188 }
189 }
190}
191
192#[async_trait]
193impl<I: Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O> for RunnableRetry<I, O>
194where
195 I: Clone,
196{
197 type Error = LcelError;
198
199 async fn invoke(
200 &self,
201 input: I,
202 config: Option<RunnableConfig>,
203 ) -> Result<O, Self::Error> {
204 if config.as_ref().is_some_and(|c| c.is_cancelled()) {
206 return Err(LcelError::Other("Operation cancelled".to_string()));
207 }
208
209 let mut last_error = None;
210
211 for attempt in 0..=self.retry_config.max_retries {
212 if attempt > 0 && config.as_ref().is_some_and(|c| c.is_cancelled()) {
214 return Err(LcelError::Other("Operation cancelled".to_string()));
215 }
216
217 if attempt > 0 {
219 let delay = self.retry_config.delay_for_attempt(attempt - 1);
220 tokio::time::sleep(delay).await;
221 }
222
223 match self.runnable.invoke_any(Box::new(input.clone()), config.clone()).await {
224 Ok(result) => {
225 return result
226 .downcast::<O>()
227 .map(|boxed| *boxed)
228 .map_err(|_| LcelError::Other("Type mismatch in retry result".to_string()));
229 }
230 Err(e) => {
231 let error_str = e.to_string();
232 if attempt < self.retry_config.max_retries && self.retry_config.should_retry(&error_str) {
233 last_error = Some(e);
234 continue;
235 }
236 return Err(e);
237 }
238 }
239 }
240
241 Err(last_error.unwrap_or_else(|| LcelError::Other("Retry exhausted with no error recorded".to_string())))
242 }
243
244 async fn stream(
245 &self,
246 input: I,
247 config: Option<RunnableConfig>,
248 ) -> Result<Pin<Box<dyn Stream<Item = Result<O, Self::Error>> + Send>>, Self::Error> {
249 let mut last_error = None;
252
253 for attempt in 0..=self.retry_config.max_retries {
254 if attempt > 0 && config.as_ref().is_some_and(|c| c.is_cancelled()) {
255 return Err(LcelError::Other("Operation cancelled".to_string()));
256 }
257
258 if attempt > 0 {
259 let delay = self.retry_config.delay_for_attempt(attempt - 1);
260 tokio::time::sleep(delay).await;
261 }
262
263 match self.runnable.stream_any(Box::new(input.clone()), config.clone()).await {
264 Ok(stream) => {
265 let typed_stream = stream.map(|result| {
267 result
268 .and_then(|boxed| {
269 boxed
270 .downcast::<O>()
271 .map(|b| *b)
272 .map_err(|_| LcelError::Other("Type mismatch in retry stream".to_string()))
273 })
274 });
275 return Ok(Box::pin(typed_stream));
276 }
277 Err(e) => {
278 let error_str = e.to_string();
279 if attempt < self.retry_config.max_retries && self.retry_config.should_retry(&error_str) {
280 last_error = Some(e);
281 continue;
282 }
283 return Err(e);
284 }
285 }
286 }
287
288 Err(last_error.unwrap_or_else(|| LcelError::Other("Retry exhausted with no error recorded".to_string())))
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::runnables::{CancellationToken, RunnableConfig, RunnableLambda, RunnableExt};
296 use std::sync::atomic::{AtomicUsize, Ordering};
297 use std::sync::Arc;
298
299 #[test]
300 fn test_retry_config_default() {
301 let config = RetryConfig::default();
302 assert_eq!(config.max_retries, 3);
303 assert_eq!(config.initial_delay, Duration::from_millis(500));
304 assert_eq!(config.max_delay, Duration::from_secs(10));
305 assert!(config.backoff_multiplier - 2.0 < f64::EPSILON);
306 }
307
308 #[test]
309 fn test_delay_for_attempt() {
310 let config = RetryConfig::default();
311 assert_eq!(config.delay_for_attempt(0), Duration::from_millis(500));
312 assert_eq!(config.delay_for_attempt(1), Duration::from_secs(1));
313 assert_eq!(config.delay_for_attempt(2), Duration::from_secs(2));
314 assert_eq!(config.delay_for_attempt(10), Duration::from_secs(10));
316 }
317
318 #[test]
319 fn test_is_transient_error() {
320 assert!(is_transient_error("HTTP 429: Too Many Requests"));
321 assert!(is_transient_error("HTTP 503: Service Unavailable"));
322 assert!(is_transient_error("rate limit exceeded"));
323 assert!(is_transient_error("Connection timeout"));
324 assert!(is_transient_error("internal server error"));
325
326 assert!(!is_transient_error("HTTP 401: Unauthorized"));
327 assert!(!is_transient_error("HTTP 403: Forbidden"));
328 assert!(!is_transient_error("invalid API key"));
329 assert!(!is_transient_error("model not found"));
330 }
331
332 #[tokio::test]
333 async fn test_retry_succeeds_on_second_attempt() {
334 let call_count = Arc::new(AtomicUsize::new(0));
335 let count_clone = call_count.clone();
336
337 let runnable = RunnableLambda::new_async(move |_: i32| {
338 let count = count_clone.clone();
339 async move {
340 let n = count.fetch_add(1, Ordering::SeqCst);
341 if n == 0 {
342 Err(LcelError::Other("HTTP 503: Service Unavailable".to_string()))
343 } else {
344 Ok(42)
345 }
346 }
347 });
348
349 let retry = runnable.with_retry(RetryConfig::new(2));
350 let result: Result<i32, _> = retry.invoke(1, None).await;
351 assert_eq!(result.unwrap(), 42);
352 assert_eq!(call_count.load(Ordering::SeqCst), 2);
353 }
354
355 #[tokio::test]
356 async fn test_retry_exhausts_all_attempts() {
357 let call_count = Arc::new(AtomicUsize::new(0));
358 let count_clone = call_count.clone();
359
360 let runnable = RunnableLambda::new_async(move |_: i32| {
361 let count = count_clone.clone();
362 async move {
363 count.fetch_add(1, Ordering::SeqCst);
364 Err(LcelError::Other("HTTP 503: Service Unavailable".to_string()))
365 }
366 });
367
368 let retry = runnable.with_retry(RetryConfig::new(2));
369 let result: Result<i32, _> = retry.invoke(1, None).await;
370 assert!(result.is_err());
371 assert_eq!(call_count.load(Ordering::SeqCst), 3); }
373
374 #[tokio::test]
375 async fn test_retry_non_retriable_error_fails_immediately() {
376 let call_count = Arc::new(AtomicUsize::new(0));
377 let count_clone = call_count.clone();
378
379 let runnable = RunnableLambda::new_async(move |_: i32| {
380 let count = count_clone.clone();
381 async move {
382 count.fetch_add(1, Ordering::SeqCst);
383 Err(LcelError::Other("HTTP 401: Unauthorized".to_string()))
384 }
385 });
386
387 let retry = runnable.with_retry(RetryConfig::new(3));
388 let result: Result<i32, _> = retry.invoke(1, None).await;
389 assert!(result.is_err());
390 assert_eq!(call_count.load(Ordering::SeqCst), 1); }
392
393 #[tokio::test]
394 async fn test_retry_succeeds_on_first_attempt() {
395 let runnable = RunnableLambda::new_sync(|x: i32| x * 2);
396 let retry = runnable.with_retry(RetryConfig::new(3));
397 let result: Result<i32, _> = retry.invoke(5, None).await;
398 assert_eq!(result.unwrap(), 10);
399 }
400
401 #[tokio::test]
402 async fn test_retry_respects_cancellation() {
403 let token = CancellationToken::new();
404 token.cancel();
405
406 let runnable = RunnableLambda::new_sync(|x: i32| x * 2);
407 let retry = runnable.with_retry(RetryConfig::new(3));
408
409 let config = RunnableConfig::new().with_cancellation_token(token);
410 let result: Result<i32, _> = retry.invoke(5, Some(config)).await;
411 assert!(result.is_err());
412 assert!(result.unwrap_err().to_string().contains("cancelled"));
413 }
414}