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(&self, input: I, config: Option<RunnableConfig>) -> Result<O, Self::Error> {
200 if config.as_ref().is_some_and(|c| c.is_cancelled()) {
202 return Err(LcelError::Other("Operation cancelled".to_string()));
203 }
204
205 let mut last_error = None;
206
207 for attempt in 0..=self.retry_config.max_retries {
208 if attempt > 0 && config.as_ref().is_some_and(|c| c.is_cancelled()) {
210 return Err(LcelError::Other("Operation cancelled".to_string()));
211 }
212
213 if attempt > 0 {
215 let delay = self.retry_config.delay_for_attempt(attempt - 1);
216 tokio::time::sleep(delay).await;
217 }
218
219 match self
220 .runnable
221 .invoke_any(Box::new(input.clone()), config.clone())
222 .await
223 {
224 Ok(result) => {
225 return result.downcast::<O>().map(|boxed| *boxed).map_err(|_| {
226 LcelError::Other("Type mismatch in retry result".to_string())
227 });
228 }
229 Err(e) => {
230 let error_str = e.to_string();
231 if attempt < self.retry_config.max_retries
232 && self.retry_config.should_retry(&error_str)
233 {
234 last_error = Some(e);
235 continue;
236 }
237 return Err(e);
238 }
239 }
240 }
241
242 Err(last_error.unwrap_or_else(|| {
243 LcelError::Other("Retry exhausted with no error recorded".to_string())
244 }))
245 }
246
247 async fn stream(
248 &self,
249 input: I,
250 config: Option<RunnableConfig>,
251 ) -> Result<Pin<Box<dyn Stream<Item = Result<O, Self::Error>> + Send>>, Self::Error> {
252 let mut last_error = None;
255
256 for attempt in 0..=self.retry_config.max_retries {
257 if attempt > 0 && config.as_ref().is_some_and(|c| c.is_cancelled()) {
258 return Err(LcelError::Other("Operation cancelled".to_string()));
259 }
260
261 if attempt > 0 {
262 let delay = self.retry_config.delay_for_attempt(attempt - 1);
263 tokio::time::sleep(delay).await;
264 }
265
266 match self
267 .runnable
268 .stream_any(Box::new(input.clone()), config.clone())
269 .await
270 {
271 Ok(stream) => {
272 let typed_stream = stream.map(|result| {
274 result.and_then(|boxed| {
275 boxed.downcast::<O>().map(|b| *b).map_err(|_| {
276 LcelError::Other("Type mismatch in retry stream".to_string())
277 })
278 })
279 });
280 return Ok(Box::pin(typed_stream));
281 }
282 Err(e) => {
283 let error_str = e.to_string();
284 if attempt < self.retry_config.max_retries
285 && self.retry_config.should_retry(&error_str)
286 {
287 last_error = Some(e);
288 continue;
289 }
290 return Err(e);
291 }
292 }
293 }
294
295 Err(last_error.unwrap_or_else(|| {
296 LcelError::Other("Retry exhausted with no error recorded".to_string())
297 }))
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::runnables::{CancellationToken, RunnableConfig, RunnableExt, RunnableLambda};
305 use std::sync::atomic::{AtomicUsize, Ordering};
306 use std::sync::Arc;
307
308 #[test]
309 fn test_retry_config_default() {
310 let config = RetryConfig::default();
311 assert_eq!(config.max_retries, 3);
312 assert_eq!(config.initial_delay, Duration::from_millis(500));
313 assert_eq!(config.max_delay, Duration::from_secs(10));
314 assert!((config.backoff_multiplier - 2.0).abs() < f64::EPSILON);
315 }
316
317 #[test]
318 fn test_delay_for_attempt() {
319 let config = RetryConfig::default();
320 assert_eq!(config.delay_for_attempt(0), Duration::from_millis(500));
321 assert_eq!(config.delay_for_attempt(1), Duration::from_secs(1));
322 assert_eq!(config.delay_for_attempt(2), Duration::from_secs(2));
323 assert_eq!(config.delay_for_attempt(10), Duration::from_secs(10));
325 }
326
327 #[test]
328 fn test_is_transient_error() {
329 assert!(is_transient_error("HTTP 429: Too Many Requests"));
330 assert!(is_transient_error("HTTP 503: Service Unavailable"));
331 assert!(is_transient_error("rate limit exceeded"));
332 assert!(is_transient_error("Connection timeout"));
333 assert!(is_transient_error("internal server error"));
334
335 assert!(!is_transient_error("HTTP 401: Unauthorized"));
336 assert!(!is_transient_error("HTTP 403: Forbidden"));
337 assert!(!is_transient_error("invalid API key"));
338 assert!(!is_transient_error("model not found"));
339 }
340
341 #[tokio::test]
342 async fn test_retry_succeeds_on_second_attempt() {
343 let call_count = Arc::new(AtomicUsize::new(0));
344 let count_clone = call_count.clone();
345
346 let runnable = RunnableLambda::new_async(move |_: i32| {
347 let count = count_clone.clone();
348 async move {
349 let n = count.fetch_add(1, Ordering::SeqCst);
350 if n == 0 {
351 Err(LcelError::Other(
352 "HTTP 503: Service Unavailable".to_string(),
353 ))
354 } else {
355 Ok(42)
356 }
357 }
358 });
359
360 let retry = runnable.with_retry(RetryConfig::new(2));
361 let result: Result<i32, _> = retry.invoke(1, None).await;
362 assert_eq!(result.unwrap(), 42);
363 assert_eq!(call_count.load(Ordering::SeqCst), 2);
364 }
365
366 #[tokio::test]
367 async fn test_retry_exhausts_all_attempts() {
368 let call_count = Arc::new(AtomicUsize::new(0));
369 let count_clone = call_count.clone();
370
371 let runnable = RunnableLambda::new_async(move |_: i32| {
372 let count = count_clone.clone();
373 async move {
374 count.fetch_add(1, Ordering::SeqCst);
375 Err(LcelError::Other(
376 "HTTP 503: Service Unavailable".to_string(),
377 ))
378 }
379 });
380
381 let retry = runnable.with_retry(RetryConfig::new(2));
382 let result: Result<i32, _> = retry.invoke(1, None).await;
383 assert!(result.is_err());
384 assert_eq!(call_count.load(Ordering::SeqCst), 3); }
386
387 #[tokio::test]
388 async fn test_retry_non_retriable_error_fails_immediately() {
389 let call_count = Arc::new(AtomicUsize::new(0));
390 let count_clone = call_count.clone();
391
392 let runnable = RunnableLambda::new_async(move |_: i32| {
393 let count = count_clone.clone();
394 async move {
395 count.fetch_add(1, Ordering::SeqCst);
396 Err(LcelError::Other("HTTP 401: Unauthorized".to_string()))
397 }
398 });
399
400 let retry = runnable.with_retry(RetryConfig::new(3));
401 let result: Result<i32, _> = retry.invoke(1, None).await;
402 assert!(result.is_err());
403 assert_eq!(call_count.load(Ordering::SeqCst), 1); }
405
406 #[tokio::test]
407 async fn test_retry_succeeds_on_first_attempt() {
408 let runnable = RunnableLambda::new_sync(|x: i32| x * 2);
409 let retry = runnable.with_retry(RetryConfig::new(3));
410 let result: Result<i32, _> = retry.invoke(5, None).await;
411 assert_eq!(result.unwrap(), 10);
412 }
413
414 #[tokio::test]
415 async fn test_retry_respects_cancellation() {
416 let token = CancellationToken::new();
417 token.cancel();
418
419 let runnable = RunnableLambda::new_sync(|x: i32| x * 2);
420 let retry = runnable.with_retry(RetryConfig::new(3));
421
422 let config = RunnableConfig::new().with_cancellation_token(token);
423 let result: Result<i32, _> = retry.invoke(5, Some(config)).await;
424 assert!(result.is_err());
425 assert!(result.unwrap_err().to_string().contains("cancelled"));
426 }
427}