Skip to main content

xmtp_common/
retry.rs

1//! A retry strategy that works with rusts native [`std::error::Error`] type.
2//!
3//! TODO: Could make the impl of `RetryableError` trait into a proc-macro to auto-derive Retryable
4//! on annotated enum variants.
5//! ```ignore
6//! #[derive(Debug, Error)]
7//! enum ErrorFoo {
8//!     #[error("I am retryable")]
9//!     #[retryable]
10//!     Retryable,
11//!     #[error("Nested errors are retryable")]
12//!     #[retryable(inherit)]
13//!     NestedRetryable(AnotherErrorWithRetryableVariants),
14//!     #[error("Always fail")]
15//!     NotRetryable
16//! }
17//! ```
18
19use crate::time::Duration;
20use crate::{MaybeSend, MaybeSync};
21use rand::RngExt;
22use std::error::Error;
23use std::sync::Arc;
24
25// Rust 1.86 added Trait upcasting, so we can add these infallible conversions
26// which is useful when getting error messages
27impl From<Box<dyn RetryableError>> for Box<dyn Error> {
28    fn from(retryable: Box<dyn RetryableError>) -> Box<dyn Error> {
29        retryable
30    }
31}
32
33// NOTE: From<> implementation is not possible here b/c of rust orphan rules (relaxed for Box
34// types)
35/// Convert an `Arc<[RetryableError]>` to a Standard Library `Arc<Error>`
36pub 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
46/// Run `future` so that every rate-limit wait of [`retry_async!`] inside it
47/// ends by `deadline`. A wait that would pass it returns the rejection at once,
48/// so a caller that times out at `deadline` still sees the delay. A nested
49/// scope keeps the earlier deadline.
50pub 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/// True when a rate-limit wait of `wait` ends before the scoped deadline of
61/// [`with_retry_deadline`], or when no deadline is scoped.
62#[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
71/// Specifies which errors are retryable.
72/// All Errors are not retryable by-default.
73pub trait RetryableError<SP = NotSpecialized>: std::error::Error + MaybeSend + MaybeSync {
74    fn is_retryable(&self) -> bool;
75
76    /// The least time to wait before a retry, when the error says so. A
77    /// rate-limit rejection carries one; [`retry_async!`] never retries sooner.
78    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/// Options to specify how to retry a function
113#[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    /// Get the number of retries this is configured with.
130    pub fn retries(&self) -> usize {
131        self.retries
132    }
133
134    /// The backoff strategy this retry uses.
135    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    /// The backoff, but at least `floor`. `None` gives up.
144    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
163/// The strategy interface
164pub trait Strategy: MaybeSend + MaybeSync {
165    /// A time that this retry should backoff
166    /// Returns None when we should no longer backoff,
167    /// despite possibly being below attempts
168    fn backoff(&self, attempts: usize, time_spent: crate::time::Instant) -> Option<Duration>;
169
170    /// The wait before the next attempt when the error asks for at least
171    /// `floor`. Returns None when the strategy gives up instead of waiting.
172    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    /// The amount to multiply the duration on each subsequent attempt
207    multiplier: u32,
208    /// Duration to be multiplied
209    duration: Duration,
210    /// jitter to add randomness
211    max_jitter: Duration,
212    /// upper limit on time to wait for all retries
213    total_wait_max: Duration,
214    /// upper limit on time to wait between retries
215    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            // total wait time == two minutes
228            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    /// Wait `max(floor, backoff)` only when the wait ends within the total
296    /// budget. A longer wait gives up at once, so the caller still sees the
297    /// error and its delay.
298    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/// Builder for [`Retry`]
310#[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
325/// Builder for [`Retry`].
326///
327/// # Example
328/// ```ignore
329/// use xmtp_common::retry::RetryBuilder;
330///
331/// RetryBuilder::default()
332///     .retries(5)
333///     .with_strategy(xmtp_common::ExponentialBackoff::default())
334///     .build();
335/// ```
336impl<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    /// Specify the  of retries to allow
351    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    /// Get the builder for [`Retry`]
366    pub fn builder() -> RetryBuilder<ExponentialBackoff> {
367        RetryBuilder::new()
368    }
369}
370
371/// Retry but for an async context
372/// ```
373/// use xmtp_common::{retry_async, retry::{RetryableError, Retry}};
374/// use thiserror::Error;
375/// use tokio::sync::mpsc;
376///
377/// #[derive(Debug, Error)]
378/// enum MyError {
379///     #[error("A retryable error")]
380///     Retryable,
381///     #[error("An error we don't want to retry")]
382///     NotRetryable
383/// }
384///
385/// impl RetryableError for MyError {
386///     fn is_retryable(&self) -> bool {
387///         match self {
388///             Self::Retryable => true,
389///             _=> false,
390///         }
391///     }
392/// }
393///
394/// async fn fallable_fn(rx: &mut mpsc::Receiver<usize>) -> Result<(), MyError> {
395///     if rx.recv().await.unwrap() == 2 {
396///         return Ok(());
397///     }
398///     Err(MyError::Retryable)
399/// }
400///
401/// #[tokio::main(flavor = "current_thread")]
402/// async fn main() -> Result<(), MyError> {
403///
404///     let (tx, mut rx) = mpsc::channel(3);
405///
406///     for i in 0..3 {
407///         tx.send(i).await.unwrap();
408///     }
409///     retry_async!(Retry::default(), (async {
410///         fallable_fn(&mut rx).await
411///     }))
412/// }
413/// ```
414#[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                        // A single retry is normal transient behavior; keep it at debug so a
432                        // flapping endpoint doesn't flood prod warn logs (only exhaustion warns).
433                        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                            // A delay too large for the clock saturates
447                            // instead of overflowing the timer's deadline.
448                            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            // do some work
575            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            // do some work
600            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    /// Run until the second attempt or until the loop gives up, on tokio's
627    /// paused clock. Returns the start of each attempt.
628    #[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    /// A strategy with no budget takes any rate-limit delay; one too large
659    /// for the clock waits without a panic, which a release build would turn
660    /// into an abort.
661    // verifies: RATE-041
662    #[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    /// On the real clock, a wait that fits the whole budget but not what is
694    /// left of it returns at once.
695    #[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    /// A rate-limit wait that would pass a scoped deadline returns at once,
722    /// even though it fits the retry budget.
723    #[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    /// Fail with `RateLimited(delay)` once, then succeed; count the attempts.
745    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    /// A nested scope keeps the earlier deadline.
766    #[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    /// A wait that ends before the scoped deadline is still taken.
793    #[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}