1use crate::time::Duration;
20use crate::{MaybeSend, MaybeSync};
21use rand::RngExt;
22use std::error::Error;
23use std::sync::Arc;
24
25impl From<Box<dyn RetryableError>> for Box<dyn Error> {
28 fn from(retryable: Box<dyn RetryableError>) -> Box<dyn Error> {
29 retryable
30 }
31}
32
33pub fn arc_retryable_to_error(retryable: Arc<dyn RetryableError>) -> Arc<dyn Error> {
37 retryable
38}
39
40pub type BoxedRetry = Retry<Box<dyn Strategy>>;
41
42tokio::task_local! {
43 static RETRY_DEADLINE: crate::time::Instant;
44}
45
46pub async fn with_retry_deadline<F: std::future::Future>(
51 deadline: crate::time::Instant,
52 future: F,
53) -> F::Output {
54 let deadline = RETRY_DEADLINE
55 .try_with(|outer| (*outer).min(deadline))
56 .unwrap_or(deadline);
57 RETRY_DEADLINE.scope(deadline, future).await
58}
59
60#[doc(hidden)]
63pub fn fits_retry_deadline(wait: Duration) -> bool {
64 RETRY_DEADLINE
65 .try_with(|deadline| deadline.saturating_duration_since(crate::time::Instant::now()) > wait)
66 .unwrap_or(true)
67}
68
69pub struct NotSpecialized;
70
71pub trait RetryableError<SP = NotSpecialized>: std::error::Error + MaybeSend + MaybeSync {
74 fn is_retryable(&self) -> bool;
75
76 fn rate_limit_delay(&self) -> Option<Duration> {
79 None
80 }
81}
82
83impl<T> RetryableError for &'_ T
84where
85 T: RetryableError,
86{
87 fn is_retryable(&self) -> bool {
88 (**self).is_retryable()
89 }
90
91 fn rate_limit_delay(&self) -> Option<Duration> {
92 (**self).rate_limit_delay()
93 }
94}
95
96impl<E: RetryableError> RetryableError for Box<E> {
97 fn is_retryable(&self) -> bool {
98 (**self).is_retryable()
99 }
100
101 fn rate_limit_delay(&self) -> Option<Duration> {
102 (**self).rate_limit_delay()
103 }
104}
105
106impl RetryableError for core::convert::Infallible {
107 fn is_retryable(&self) -> bool {
108 unreachable!()
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct Retry<S = ExponentialBackoff> {
115 retries: usize,
116 strategy: S,
117}
118
119impl Default for Retry {
120 fn default() -> Retry {
121 Retry {
122 retries: 5,
123 strategy: ExponentialBackoff::default(),
124 }
125 }
126}
127
128impl<S: Strategy> Retry<S> {
129 pub fn retries(&self) -> usize {
131 self.retries
132 }
133
134 pub fn strategy(&self) -> &S {
136 &self.strategy
137 }
138
139 pub fn backoff(&self, attempts: usize, time_spent: crate::time::Instant) -> Option<Duration> {
140 self.strategy.backoff(attempts, time_spent)
141 }
142
143 pub fn backoff_at_least(
145 &self,
146 attempts: usize,
147 time_spent: crate::time::Instant,
148 floor: Duration,
149 ) -> Option<Duration> {
150 self.strategy.backoff_at_least(attempts, time_spent, floor)
151 }
152}
153
154impl<S: Strategy + 'static> Retry<S> {
155 pub fn boxed(self) -> Retry<Box<dyn Strategy>> {
156 Retry {
157 strategy: Box::new(self.strategy),
158 retries: self.retries,
159 }
160 }
161}
162
163pub trait Strategy: MaybeSend + MaybeSync {
165 fn backoff(&self, attempts: usize, time_spent: crate::time::Instant) -> Option<Duration>;
169
170 fn backoff_at_least(
173 &self,
174 attempts: usize,
175 time_spent: crate::time::Instant,
176 floor: Duration,
177 ) -> Option<Duration> {
178 self.backoff(attempts, time_spent)
179 .map(|wait| wait.max(floor))
180 }
181}
182
183impl Strategy for () {
184 fn backoff(&self, _attempts: usize, _time_spent: crate::time::Instant) -> Option<Duration> {
185 Some(Duration::ZERO)
186 }
187}
188
189impl<S: ?Sized + Strategy> Strategy for Box<S> {
190 fn backoff(&self, attempts: usize, time_spent: crate::time::Instant) -> Option<Duration> {
191 (**self).backoff(attempts, time_spent)
192 }
193
194 fn backoff_at_least(
195 &self,
196 attempts: usize,
197 time_spent: crate::time::Instant,
198 floor: Duration,
199 ) -> Option<Duration> {
200 (**self).backoff_at_least(attempts, time_spent, floor)
201 }
202}
203
204#[derive(Clone, Debug)]
205pub struct ExponentialBackoff {
206 multiplier: u32,
208 duration: Duration,
210 max_jitter: Duration,
212 total_wait_max: Duration,
214 individual_wait_max: Duration,
216}
217
218impl ExponentialBackoff {
219 pub fn builder() -> ExponentialBackoffBuilder {
220 ExponentialBackoffBuilder::default()
221 }
222}
223
224impl Default for ExponentialBackoff {
225 fn default() -> Self {
226 Self {
227 multiplier: 3,
229 duration: Duration::from_millis(50),
230 total_wait_max: Duration::from_secs(120),
231 individual_wait_max: Duration::from_secs(30),
232 max_jitter: Duration::from_millis(25),
233 }
234 }
235}
236
237#[derive(Default)]
238pub struct ExponentialBackoffBuilder {
239 duration: Option<Duration>,
240 max_jitter: Option<Duration>,
241 multiplier: Option<u32>,
242 total_wait_max: Option<Duration>,
243}
244
245impl ExponentialBackoffBuilder {
246 pub fn duration(mut self, duration: Duration) -> Self {
247 self.duration = Some(duration);
248 self
249 }
250
251 pub fn max_jitter(mut self, max_jitter: Duration) -> Self {
252 self.max_jitter = Some(max_jitter);
253 self
254 }
255
256 pub fn multiplier(mut self, multiplier: u32) -> Self {
257 self.multiplier = Some(multiplier);
258 self
259 }
260
261 pub fn total_wait_max(mut self, total_wait_max: Duration) -> Self {
262 self.total_wait_max = Some(total_wait_max);
263 self
264 }
265
266 pub fn build(self) -> ExponentialBackoff {
267 ExponentialBackoff {
268 duration: self.duration.unwrap_or(Duration::from_millis(25)),
269 max_jitter: self.max_jitter.unwrap_or(Duration::from_millis(25)),
270 multiplier: self.multiplier.unwrap_or(3),
271 total_wait_max: self.total_wait_max.unwrap_or(Duration::from_secs(120)),
272 individual_wait_max: Duration::from_secs(30),
273 }
274 }
275}
276
277impl Strategy for ExponentialBackoff {
278 fn backoff(&self, attempts: usize, time_spent: crate::time::Instant) -> Option<Duration> {
279 if time_spent.elapsed() > self.total_wait_max {
280 return None;
281 }
282 let mut duration = self.duration;
283 for _ in 0..(attempts.saturating_sub(1)) {
284 duration *= self.multiplier;
285 if duration > self.individual_wait_max {
286 duration = self.individual_wait_max;
287 }
288 }
289 let distr = rand::distr::Uniform::new_inclusive(Duration::ZERO, self.max_jitter).unwrap();
290 let jitter = rand::rng().sample(distr);
291 let wait = duration + jitter;
292 Some(wait)
293 }
294
295 fn backoff_at_least(
299 &self,
300 attempts: usize,
301 time_spent: crate::time::Instant,
302 floor: Duration,
303 ) -> Option<Duration> {
304 let wait = self.backoff(attempts, time_spent)?.max(floor);
305 (time_spent.elapsed().saturating_add(wait) <= self.total_wait_max).then_some(wait)
306 }
307}
308
309#[derive(Default, Debug, Copy, Clone)]
311pub struct RetryBuilder<S> {
312 retries: Option<usize>,
313 strategy: S,
314}
315
316impl RetryBuilder<ExponentialBackoff> {
317 pub fn new() -> Self {
318 Self {
319 retries: Some(5),
320 strategy: ExponentialBackoff::default(),
321 }
322 }
323}
324
325impl<S: Strategy> RetryBuilder<S> {
337 pub fn build(self) -> Retry<S> {
338 let mut retry = Retry {
339 retries: 5usize,
340 strategy: self.strategy,
341 };
342
343 if let Some(retries) = self.retries {
344 retry.retries = retries;
345 }
346
347 retry
348 }
349
350 pub fn retries(mut self, retries: usize) -> Self {
352 self.retries = Some(retries);
353 self
354 }
355
356 pub fn with_strategy<St: Strategy>(self, strategy: St) -> RetryBuilder<St> {
357 RetryBuilder {
358 retries: self.retries,
359 strategy,
360 }
361 }
362}
363
364impl Retry {
365 pub fn builder() -> RetryBuilder<ExponentialBackoff> {
367 RetryBuilder::new()
368 }
369}
370
371#[macro_export]
415macro_rules! retry_async {
416 ($retry: expr, $code: tt) => {{
417 use tracing::Instrument as _;
418 #[allow(unused)]
419 use $crate::retry::RetryableError;
420 let mut attempts = 0;
421 let time_spent = $crate::time::Instant::now();
422 let span = tracing::trace_span!("retry");
423 loop {
424 let span = span.clone();
425 #[allow(clippy::redundant_closure_call)]
426 let res = $code.instrument(span).await;
427 match res {
428 Ok(v) => break Ok(v),
429 Err(e) => {
430 if (&e).is_retryable() && attempts < $retry.retries() {
431 tracing::debug!(
434 attempt = attempts,
435 "retrying function that failed with error={}",
436 e.to_string()
437 );
438 let wait = match (&e).rate_limit_delay() {
439 Some(floor) => $retry
440 .backoff_at_least(attempts, time_spent, floor)
441 .filter(|wait| $crate::retry::fits_retry_deadline(*wait)),
442 None => $retry.backoff(attempts, time_spent),
443 };
444 if let Some(d) = wait {
445 attempts += 1;
446 let now = $crate::time::Instant::now();
449 let until = $crate::time::saturating_after(now, d);
450 $crate::time::sleep(until.saturating_duration_since(now)).await;
451 } else {
452 tracing::warn!(
453 attempts,
454 elapsed_ms = time_spent.elapsed().as_millis(),
455 "retry strategy exceeded max wait time, giving up: {}",
456 e.to_string()
457 );
458 break Err(e);
459 }
460 } else {
461 tracing::trace!("error is not retryable. {}", e);
462 break Err(e);
463 }
464 }
465 }
466 }
467 }};
468}
469
470#[macro_export]
471macro_rules! retryable {
472 ($error: ident) => {{
473 #[allow(unused)]
474 use $crate::retry::RetryableError;
475 $error.is_retryable()
476 }};
477 ($error: expr) => {{
478 use $crate::retry::RetryableError;
479 $error.is_retryable()
480 }};
481}
482
483#[cfg(test)]
484pub(crate) mod tests {
485 use super::*;
486
487 use thiserror::Error;
488 use tokio::sync::mpsc;
489
490 #[derive(Debug, Error)]
491 enum SomeError {
492 #[error("this is a retryable error")]
493 ARetryableError,
494 #[error("Dont retry")]
495 DontRetryThis,
496 }
497
498 impl RetryableError for SomeError {
499 fn is_retryable(&self) -> bool {
500 matches!(self, Self::ARetryableError)
501 }
502 }
503
504 fn retry_error_fn() -> Result<(), SomeError> {
505 Err(SomeError::ARetryableError)
506 }
507
508 fn retryable_with_args(foo: usize, name: String, list: &Vec<String>) -> Result<(), SomeError> {
509 println!("I am {foo} of {name} with items {list:?}");
510 Err(SomeError::ARetryableError)
511 }
512
513 #[xmtp_macro::test]
514 async fn it_retries_twice_and_succeeds() {
515 let mut i = 0;
516 let mut test_fn = || -> Result<(), SomeError> {
517 if i == 2 {
518 return Ok(());
519 }
520 i += 1;
521 retry_error_fn()?;
522 Ok(())
523 };
524
525 retry_async!(Retry::default(), (async { test_fn() })).unwrap();
526 }
527
528 #[xmtp_macro::test]
529 async fn it_works_with_random_args() {
530 let mut i = 0;
531 let list = vec!["String".into(), "Foo".into()];
532 let mut test_fn = || -> Result<(), SomeError> {
533 if i == 2 {
534 return Ok(());
535 }
536 i += 1;
537 retryable_with_args(i, "Hello".to_string(), &list)
538 };
539
540 retry_async!(Retry::default(), (async { test_fn() })).unwrap();
541 }
542
543 #[xmtp_macro::test]
544 async fn it_fails_on_three_retries() {
545 let closure = || -> Result<(), SomeError> {
546 retry_error_fn()?;
547 Ok(())
548 };
549 let result: Result<(), SomeError> = retry_async!(Retry::default(), (async { closure() }));
550
551 assert!(result.is_err())
552 }
553
554 #[xmtp_macro::test]
555 async fn it_only_runs_non_retryable_once() {
556 let mut attempts = 0;
557 let mut test_fn = || -> Result<(), SomeError> {
558 attempts += 1;
559 Err(SomeError::DontRetryThis)
560 };
561
562 let _r = retry_async!(Retry::default(), (async { test_fn() }));
563
564 assert_eq!(attempts, 1);
565 }
566
567 #[xmtp_macro::test]
568 async fn it_works_async() {
569 async fn retryable_async_fn(rx: &mut mpsc::Receiver<usize>) -> Result<(), SomeError> {
570 let val = rx.recv().await.unwrap();
571 if val == 2 {
572 return Ok(());
573 }
574 crate::time::sleep(core::time::Duration::from_nanos(100)).await;
576 Err(SomeError::ARetryableError)
577 }
578
579 let (tx, mut rx) = mpsc::channel(3);
580
581 for i in 0..3 {
582 tx.send(i).await.unwrap();
583 }
584 retry_async!(
585 Retry::default(),
586 (async { retryable_async_fn(&mut rx).await })
587 )
588 .unwrap();
589 assert!(rx.is_empty());
590 }
591
592 #[xmtp_macro::test]
593 async fn it_works_async_mut() {
594 async fn retryable_async_fn(data: &mut usize) -> Result<(), SomeError> {
595 if *data == 2 {
596 return Ok(());
597 }
598 *data += 1;
599 crate::time::sleep(core::time::Duration::from_nanos(100)).await;
601 Err(SomeError::ARetryableError)
602 }
603
604 let mut data: usize = 0;
605 retry_async!(
606 Retry::default(),
607 (async { retryable_async_fn(&mut data).await })
608 )
609 .unwrap();
610 }
611
612 #[derive(Debug, Error)]
613 #[error("rate limited")]
614 struct RateLimited(Duration);
615
616 impl RetryableError for RateLimited {
617 fn is_retryable(&self) -> bool {
618 true
619 }
620
621 fn rate_limit_delay(&self) -> Option<Duration> {
622 Some(self.0)
623 }
624 }
625
626 #[cfg(not(target_arch = "wasm32"))]
629 async fn attempt_starts(
630 delay: Duration,
631 ) -> (Vec<tokio::time::Instant>, Result<(), RateLimited>) {
632 tokio::time::pause();
633 let mut starts = Vec::new();
634 let result = retry_async!(
635 Retry::default(),
636 (async {
637 starts.push(tokio::time::Instant::now());
638 if starts.len() == 1 {
639 Err(RateLimited(delay))
640 } else {
641 Ok(())
642 }
643 })
644 );
645 (starts, result)
646 }
647
648 #[cfg(not(target_arch = "wasm32"))]
649 #[xmtp_macro::test]
650 async fn rate_limited_retry_waits_the_delay() {
651 let delay = Duration::from_secs(5);
652 let (starts, result) = attempt_starts(delay).await;
653 assert!(result.is_ok());
654 assert_eq!(starts.len(), 2);
655 assert!(starts[1] - starts[0] >= delay);
656 }
657
658 #[cfg(not(target_arch = "wasm32"))]
663 #[xmtp_macro::test]
664 async fn an_out_of_range_delay_waits_without_panicking() {
665 tokio::time::pause();
666 let attempts = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
667 let counted = attempts.clone();
668 let retry = tokio::spawn(async move {
669 retry_async!(
670 Retry::builder().with_strategy(()).retries(1).build(),
671 (async {
672 counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
673 Err::<(), _>(RateLimited(Duration::from_secs(u64::MAX)))
674 })
675 )
676 });
677 tokio::time::sleep(Duration::from_secs(365 * 24 * 60 * 60)).await;
678 assert_eq!(attempts.load(std::sync::atomic::Ordering::SeqCst), 1);
679 assert!(!retry.is_finished(), "the retry waits out the delay");
680 retry.abort();
681 assert!(retry.await.unwrap_err().is_cancelled(), "it did not panic");
682 }
683
684 #[cfg(not(target_arch = "wasm32"))]
685 #[xmtp_macro::test]
686 async fn rate_limit_delay_beyond_the_budget_returns_at_once() {
687 let (starts, result) = attempt_starts(Duration::from_secs(200)).await;
688 assert_eq!(result.unwrap_err().0, Duration::from_secs(200));
689 assert_eq!(starts.len(), 1);
690 assert_eq!(starts[0], tokio::time::Instant::now());
691 }
692
693 #[cfg(not(target_arch = "wasm32"))]
696 #[xmtp_macro::test]
697 async fn a_rate_limit_wait_past_the_remaining_budget_returns_at_once() {
698 let retry = Retry::builder()
699 .retries(5)
700 .with_strategy(
701 ExponentialBackoff::builder()
702 .duration(Duration::ZERO)
703 .max_jitter(Duration::ZERO)
704 .total_wait_max(Duration::from_millis(300))
705 .build(),
706 )
707 .build();
708 let mut attempts = 0;
709 let result: Result<(), RateLimited> = retry_async!(
710 retry,
711 (async {
712 attempts += 1;
713 crate::time::sleep(Duration::from_millis(200)).await;
714 Err(RateLimited(Duration::from_millis(150)))
715 })
716 );
717 assert!(result.is_err());
718 assert_eq!(attempts, 1);
719 }
720
721 #[cfg(not(target_arch = "wasm32"))]
724 #[xmtp_macro::test]
725 async fn a_rate_limit_wait_past_a_scoped_deadline_returns_at_once() {
726 let mut attempts = 0;
727 let result: Result<(), RateLimited> = with_retry_deadline(
728 crate::time::Instant::now() + Duration::from_secs(1),
729 async {
730 retry_async!(
731 Retry::default(),
732 (async {
733 attempts += 1;
734 Err(RateLimited(Duration::from_secs(5)))
735 })
736 )
737 },
738 )
739 .await;
740 assert_eq!(result.unwrap_err().0, Duration::from_secs(5));
741 assert_eq!(attempts, 1);
742 }
743
744 async fn retry_once_under(deadline: crate::time::Instant, delay: Duration) -> usize {
746 let mut attempts = 0;
747 let result: Result<(), RateLimited> = with_retry_deadline(deadline, async {
748 retry_async!(
749 Retry::default(),
750 (async {
751 attempts += 1;
752 if attempts == 1 {
753 Err(RateLimited(delay))
754 } else {
755 Ok(())
756 }
757 })
758 )
759 })
760 .await;
761 let _ = result;
762 attempts
763 }
764
765 #[xmtp_macro::test]
767 async fn a_nested_retry_deadline_keeps_the_earlier_one() {
768 let now = crate::time::Instant::now();
769 let mut attempts = 0;
770 let result: Result<(), RateLimited> =
771 with_retry_deadline(now + Duration::from_secs(10), async {
772 with_retry_deadline(now + Duration::from_secs(60), async {
773 retry_async!(
774 Retry::default(),
775 (async {
776 attempts += 1;
777 if attempts == 1 {
778 Err(RateLimited(Duration::from_secs(20)))
779 } else {
780 Ok(())
781 }
782 })
783 )
784 })
785 .await
786 })
787 .await;
788 assert!(result.is_err());
789 assert_eq!(attempts, 1);
790 }
791
792 #[xmtp_macro::test]
794 async fn a_rate_limit_wait_inside_a_scoped_deadline_is_taken() {
795 let deadline = crate::time::Instant::now() + Duration::from_secs(10);
796 assert_eq!(
797 retry_once_under(deadline, Duration::from_millis(50)).await,
798 2
799 );
800 }
801
802 #[xmtp_macro::test]
803 fn backoff_at_least_keeps_the_longer_backoff() {
804 let retry = Retry::default();
805 let time_spent = crate::time::Instant::now();
806 let wait = retry
807 .backoff_at_least(3, time_spent, Duration::from_millis(1))
808 .unwrap();
809 assert!(wait >= Duration::from_millis(450));
810 }
811
812 #[xmtp_macro::test]
813 fn backoff_retry() {
814 let backoff_retry = Retry::default();
815 let time_spent = crate::time::Instant::now();
816 assert!(backoff_retry.backoff(1, time_spent).unwrap().as_millis() - 50 <= 25);
817 assert!(backoff_retry.backoff(2, time_spent).unwrap().as_millis() - 150 <= 25);
818 assert!(backoff_retry.backoff(3, time_spent).unwrap().as_millis() - 450 <= 25);
819 }
820}