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 pub fn validate(&self) -> Result<(), String> {
127 if !self.backoff_multiplier.is_finite() || self.backoff_multiplier <= 0.0 {
128 return Err(format!(
129 "backoff_multiplier must be a finite, positive number, got {}",
130 self.backoff_multiplier
131 ));
132 }
133 if self.max_delay < self.initial_delay {
134 return Err(format!(
135 "max_delay ({:?}) must be at least initial_delay ({:?})",
136 self.max_delay, self.initial_delay
137 ));
138 }
139 Ok(())
140 }
141
142 fn should_retry(&self, error: &str) -> bool {
144 match &self.retry_on {
145 RetryOn::AllErrors => true,
146 RetryOn::TransientErrors => is_transient_error(error),
147 RetryOn::Custom(predicate) => predicate(error),
148 }
149 }
150
151 fn delay_for_attempt(&self, attempt: usize) -> Duration {
153 let multiplier = self.backoff_multiplier.powi(attempt as i32);
154 let delay = (self.initial_delay.as_secs_f64() * multiplier).max(0.0);
157 let delay = delay.min(self.max_delay.as_secs_f64()).max(0.0);
158 Duration::from_secs_f64(delay)
159 }
160}
161
162fn is_transient_error(error: &str) -> bool {
164 let error_lower = error.to_lowercase();
165
166 for code in &["429", "500", "502", "503", "504"] {
171 if error_lower
172 .split(|c: char| !c.is_alphanumeric())
173 .any(|t| t == *code)
174 {
175 return true;
176 }
177 }
178
179 let transient_patterns = [
181 "rate limit",
182 "rate_limit",
183 "ratelimit",
184 "too many requests",
185 "timeout",
186 "timed out",
187 "connection reset",
188 "connection refused",
189 "temporary failure",
190 "service unavailable",
191 "internal server error",
192 "overloaded",
193 "capacity",
194 ];
195
196 for pattern in &transient_patterns {
197 if error_lower.contains(pattern) {
198 return true;
199 }
200 }
201
202 false
203}
204
205pub struct RunnableRetry<I: Send + Sync + 'static, O: Send + Sync + 'static> {
207 runnable: Arc<dyn RunnableAny>,
208 retry_config: RetryConfig,
209 _marker: std::marker::PhantomData<(I, O)>,
210}
211
212impl<I: Send + Sync + 'static, O: Send + Sync + 'static> RunnableRetry<I, O> {
213 pub fn new(runnable: Box<dyn RunnableAny>, retry_config: RetryConfig) -> Result<Self, String> {
218 retry_config.validate()?;
219 Ok(Self {
220 runnable: Arc::from(runnable),
221 retry_config,
222 _marker: std::marker::PhantomData,
223 })
224 }
225}
226
227#[async_trait]
228impl<I: Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O> for RunnableRetry<I, O>
229where
230 I: Clone,
231{
232 type Error = LcelError;
233
234 async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<O, Self::Error> {
235 if config.as_ref().is_some_and(|c| c.is_cancelled()) {
237 return Err(LcelError::Other("Operation cancelled".to_string()));
238 }
239
240 let mut last_error = None;
241
242 for attempt in 0..=self.retry_config.max_retries {
243 if attempt > 0 && config.as_ref().is_some_and(|c| c.is_cancelled()) {
245 return Err(LcelError::Other("Operation cancelled".to_string()));
246 }
247
248 if attempt > 0 {
250 let delay = self.retry_config.delay_for_attempt(attempt - 1);
251 tokio::time::sleep(delay).await;
252 }
253
254 match self
255 .runnable
256 .invoke_any(Box::new(input.clone()), config.clone())
257 .await
258 {
259 Ok(result) => {
260 return result.downcast::<O>().map(|boxed| *boxed).map_err(|_| {
261 LcelError::Other("Type mismatch in retry result".to_string())
262 });
263 }
264 Err(e) => {
265 let error_str = e.to_string();
266 if attempt < self.retry_config.max_retries
267 && self.retry_config.should_retry(&error_str)
268 {
269 last_error = Some(e);
270 continue;
271 }
272 return Err(e);
273 }
274 }
275 }
276
277 Err(last_error.unwrap_or_else(|| {
278 LcelError::Other("Retry exhausted with no error recorded".to_string())
279 }))
280 }
281
282 async fn stream(
283 &self,
284 input: I,
285 config: Option<RunnableConfig>,
286 ) -> Result<Pin<Box<dyn Stream<Item = Result<O, Self::Error>> + Send>>, Self::Error> {
287 let mut last_error = None;
290
291 for attempt in 0..=self.retry_config.max_retries {
292 if attempt > 0 && config.as_ref().is_some_and(|c| c.is_cancelled()) {
293 return Err(LcelError::Other("Operation cancelled".to_string()));
294 }
295
296 if attempt > 0 {
297 let delay = self.retry_config.delay_for_attempt(attempt - 1);
298 tokio::time::sleep(delay).await;
299 }
300
301 match self
302 .runnable
303 .stream_any(Box::new(input.clone()), config.clone())
304 .await
305 {
306 Ok(stream) => {
307 let typed_stream = stream.map(|result| {
309 result.and_then(|boxed| {
310 boxed.downcast::<O>().map(|b| *b).map_err(|_| {
311 LcelError::Other("Type mismatch in retry stream".to_string())
312 })
313 })
314 });
315 return Ok(Box::pin(typed_stream));
316 }
317 Err(e) => {
318 let error_str = e.to_string();
319 if attempt < self.retry_config.max_retries
320 && self.retry_config.should_retry(&error_str)
321 {
322 last_error = Some(e);
323 continue;
324 }
325 return Err(e);
326 }
327 }
328 }
329
330 Err(last_error.unwrap_or_else(|| {
331 LcelError::Other("Retry exhausted with no error recorded".to_string())
332 }))
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::runnables::{CancellationToken, RunnableConfig, RunnableExt, RunnableLambda};
340 use std::sync::atomic::{AtomicUsize, Ordering};
341 use std::sync::Arc;
342
343 #[test]
344 fn test_retry_config_default() {
345 let config = RetryConfig::default();
346 assert_eq!(config.max_retries, 3);
347 assert_eq!(config.initial_delay, Duration::from_millis(500));
348 assert_eq!(config.max_delay, Duration::from_secs(10));
349 assert!((config.backoff_multiplier - 2.0).abs() < f64::EPSILON);
350 }
351
352 #[test]
353 fn test_delay_for_attempt() {
354 let config = RetryConfig::default();
355 assert_eq!(config.delay_for_attempt(0), Duration::from_millis(500));
356 assert_eq!(config.delay_for_attempt(1), Duration::from_secs(1));
357 assert_eq!(config.delay_for_attempt(2), Duration::from_secs(2));
358 assert_eq!(config.delay_for_attempt(10), Duration::from_secs(10));
360 }
361
362 #[test]
363 fn test_is_transient_error() {
364 assert!(is_transient_error("HTTP 429: Too Many Requests"));
365 assert!(is_transient_error("HTTP 503: Service Unavailable"));
366 assert!(is_transient_error("rate limit exceeded"));
367 assert!(is_transient_error("Connection timeout"));
368 assert!(is_transient_error("internal server error"));
369
370 assert!(!is_transient_error("HTTP 401: Unauthorized"));
371 assert!(!is_transient_error("HTTP 403: Forbidden"));
372 assert!(!is_transient_error("invalid API key"));
373 assert!(!is_transient_error("model not found"));
374 }
375
376 #[tokio::test]
377 async fn test_retry_succeeds_on_second_attempt() {
378 let call_count = Arc::new(AtomicUsize::new(0));
379 let count_clone = call_count.clone();
380
381 let runnable = RunnableLambda::new_async(move |_: i32| {
382 let count = count_clone.clone();
383 async move {
384 let n = count.fetch_add(1, Ordering::SeqCst);
385 if n == 0 {
386 Err(LcelError::Other(
387 "HTTP 503: Service Unavailable".to_string(),
388 ))
389 } else {
390 Ok(42)
391 }
392 }
393 });
394
395 let retry = runnable.with_retry(RetryConfig::new(2)).unwrap();
396 let result: Result<i32, _> = retry.invoke(1, None).await;
397 assert_eq!(result.unwrap(), 42);
398 assert_eq!(call_count.load(Ordering::SeqCst), 2);
399 }
400
401 #[tokio::test]
402 async fn test_retry_exhausts_all_attempts() {
403 let call_count = Arc::new(AtomicUsize::new(0));
404 let count_clone = call_count.clone();
405
406 let runnable = RunnableLambda::new_async(move |_: i32| {
407 let count = count_clone.clone();
408 async move {
409 count.fetch_add(1, Ordering::SeqCst);
410 Err(LcelError::Other(
411 "HTTP 503: Service Unavailable".to_string(),
412 ))
413 }
414 });
415
416 let retry = runnable.with_retry(RetryConfig::new(2)).unwrap();
417 let result: Result<i32, _> = retry.invoke(1, None).await;
418 assert!(result.is_err());
419 assert_eq!(call_count.load(Ordering::SeqCst), 3); }
421
422 #[tokio::test]
423 async fn test_retry_non_retriable_error_fails_immediately() {
424 let call_count = Arc::new(AtomicUsize::new(0));
425 let count_clone = call_count.clone();
426
427 let runnable = RunnableLambda::new_async(move |_: i32| {
428 let count = count_clone.clone();
429 async move {
430 count.fetch_add(1, Ordering::SeqCst);
431 Err(LcelError::Other("HTTP 401: Unauthorized".to_string()))
432 }
433 });
434
435 let retry = runnable.with_retry(RetryConfig::new(3)).unwrap();
436 let result: Result<i32, _> = retry.invoke(1, None).await;
437 assert!(result.is_err());
438 assert_eq!(call_count.load(Ordering::SeqCst), 1); }
440
441 #[tokio::test]
442 async fn test_retry_succeeds_on_first_attempt() {
443 let runnable = RunnableLambda::new_sync(|x: i32| x * 2);
444 let retry = runnable.with_retry(RetryConfig::new(3)).unwrap();
445 let result: Result<i32, _> = retry.invoke(5, None).await;
446 assert_eq!(result.unwrap(), 10);
447 }
448
449 #[tokio::test]
450 async fn test_retry_respects_cancellation() {
451 let token = CancellationToken::new();
452 token.cancel();
453
454 let runnable = RunnableLambda::new_sync(|x: i32| x * 2);
455 let retry = runnable.with_retry(RetryConfig::new(3)).unwrap();
456
457 let config = RunnableConfig::new().with_cancellation_token(token);
458 let result: Result<i32, _> = retry.invoke(5, Some(config)).await;
459 assert!(result.is_err());
460 assert!(result.unwrap_err().to_string().contains("cancelled"));
461 }
462
463 #[test]
466 fn validate_accepts_default_config() {
467 assert!(RetryConfig::default().validate().is_ok());
468 }
469
470 #[test]
471 fn validate_rejects_non_positive_or_non_finite_multiplier() {
472 for bad in [0.0, -1.0, -2.5, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
473 let cfg = RetryConfig::default().with_backoff_multiplier(bad);
474 assert!(
475 cfg.validate().is_err(),
476 "multiplier {bad} must be rejected at construction"
477 );
478 }
479 }
480
481 #[test]
482 fn validate_rejects_delay_bounds_inverted() {
483 let cfg = RetryConfig::default()
484 .with_initial_delay(Duration::from_secs(30))
485 .with_max_delay(Duration::from_millis(100));
486 assert!(cfg.validate().is_err());
487 }
488
489 #[tokio::test]
490 async fn with_retry_rejects_invalid_config_before_running() {
491 let runnable = RunnableLambda::new_sync(|x: i32| x * 2);
492 let result = runnable.with_retry(RetryConfig::default().with_backoff_multiplier(-1.0));
495 assert!(result.is_err());
496 }
497}