1use crate::error::{AptosError, AptosResult};
26use std::collections::HashSet;
27use std::future::Future;
28use std::sync::Arc;
29use std::time::Duration;
30use tokio::time::sleep;
31
32#[derive(Debug, Clone)]
34pub struct RetryConfig {
35 pub max_retries: u32,
37 pub initial_delay_ms: u64,
39 pub max_delay_ms: u64,
41 pub exponential_base: f64,
43 pub jitter: bool,
45 pub jitter_factor: f64,
47 pub retryable_status_codes: HashSet<u16>,
50}
51
52impl Default for RetryConfig {
53 fn default() -> Self {
54 Self {
55 max_retries: 3,
56 initial_delay_ms: 100,
57 max_delay_ms: 10_000,
58 exponential_base: 2.0,
59 jitter: true,
60 jitter_factor: 0.5,
61 retryable_status_codes: [
62 408, 429, 500, 502, 503, 504, ]
69 .into_iter()
70 .collect(),
71 }
72 }
73}
74
75impl RetryConfig {
76 pub fn builder() -> RetryConfigBuilder {
78 RetryConfigBuilder::default()
79 }
80
81 pub fn no_retry() -> Self {
83 Self {
84 max_retries: 0,
85 ..Default::default()
86 }
87 }
88
89 pub fn aggressive() -> Self {
91 Self {
92 max_retries: 5,
93 initial_delay_ms: 50,
94 max_delay_ms: 5_000,
95 exponential_base: 1.5,
96 jitter: true,
97 jitter_factor: 0.3,
98 ..Default::default()
99 }
100 }
101
102 pub fn conservative() -> Self {
104 Self {
105 max_retries: 3,
106 initial_delay_ms: 500,
107 max_delay_ms: 30_000,
108 exponential_base: 2.0,
109 jitter: true,
110 jitter_factor: 0.5,
111 ..Default::default()
112 }
113 }
114
115 #[allow(clippy::cast_possible_truncation)] pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
118 if attempt == 0 {
119 return Duration::from_millis(0);
120 }
121
122 #[allow(clippy::cast_precision_loss)] let base_delay = self.initial_delay_ms as f64
125 * self
126 .exponential_base
127 .powi(attempt.saturating_sub(1).cast_signed());
128
129 #[allow(clippy::cast_precision_loss)] let capped_delay = base_delay.min(self.max_delay_ms as f64);
132
133 let final_delay = if self.jitter {
135 let jitter_range = capped_delay * self.jitter_factor;
136 let jitter = rand::random::<f64>() * jitter_range * 2.0 - jitter_range;
137 (capped_delay + jitter).max(0.0)
138 } else {
139 capped_delay
140 };
141
142 #[allow(clippy::cast_sign_loss)] Duration::from_millis(final_delay as u64)
144 }
145
146 #[inline]
148 pub fn is_retryable_status(&self, status_code: u16) -> bool {
149 self.retryable_status_codes.contains(&status_code)
150 }
151
152 #[inline]
154 pub fn is_retryable_error(&self, error: &AptosError) -> bool {
155 match error {
156 AptosError::Http(_) | AptosError::RateLimited { .. } => true,
158 AptosError::Api { status_code, .. } => self.is_retryable_status(*status_code),
160 _ => false,
162 }
163 }
164}
165
166#[derive(Debug, Clone, Default)]
168pub struct RetryConfigBuilder {
169 max_retries: Option<u32>,
170 initial_delay_ms: Option<u64>,
171 max_delay_ms: Option<u64>,
172 exponential_base: Option<f64>,
173 jitter: Option<bool>,
174 jitter_factor: Option<f64>,
175 retryable_status_codes: Option<HashSet<u16>>,
176}
177
178impl RetryConfigBuilder {
179 #[must_use]
181 pub fn max_retries(mut self, max_retries: u32) -> Self {
182 self.max_retries = Some(max_retries);
183 self
184 }
185
186 #[must_use]
188 pub fn initial_delay_ms(mut self, initial_delay_ms: u64) -> Self {
189 self.initial_delay_ms = Some(initial_delay_ms);
190 self
191 }
192
193 #[must_use]
195 pub fn max_delay_ms(mut self, max_delay_ms: u64) -> Self {
196 self.max_delay_ms = Some(max_delay_ms);
197 self
198 }
199
200 #[must_use]
202 pub fn exponential_base(mut self, base: f64) -> Self {
203 self.exponential_base = Some(base);
204 self
205 }
206
207 #[must_use]
209 pub fn jitter(mut self, jitter: bool) -> Self {
210 self.jitter = Some(jitter);
211 self
212 }
213
214 #[must_use]
216 pub fn jitter_factor(mut self, factor: f64) -> Self {
217 self.jitter_factor = Some(factor.clamp(0.0, 1.0));
218 self
219 }
220
221 #[must_use]
223 pub fn retryable_status_codes(mut self, codes: impl IntoIterator<Item = u16>) -> Self {
224 self.retryable_status_codes = Some(codes.into_iter().collect());
225 self
226 }
227
228 #[must_use]
230 pub fn add_retryable_status_code(mut self, code: u16) -> Self {
231 let mut codes = self.retryable_status_codes.unwrap_or_default();
232 codes.insert(code);
233 self.retryable_status_codes = Some(codes);
234 self
235 }
236
237 #[must_use]
239 pub fn build(self) -> RetryConfig {
240 let default = RetryConfig::default();
241 RetryConfig {
242 max_retries: self.max_retries.unwrap_or(default.max_retries),
243 initial_delay_ms: self.initial_delay_ms.unwrap_or(default.initial_delay_ms),
244 max_delay_ms: self.max_delay_ms.unwrap_or(default.max_delay_ms),
245 exponential_base: self.exponential_base.unwrap_or(default.exponential_base),
246 jitter: self.jitter.unwrap_or(default.jitter),
247 jitter_factor: self.jitter_factor.unwrap_or(default.jitter_factor),
248 retryable_status_codes: self
249 .retryable_status_codes
250 .unwrap_or(default.retryable_status_codes),
251 }
252 }
253}
254
255#[derive(Debug, Clone)]
257pub struct RetryExecutor {
258 config: Arc<RetryConfig>,
259}
260
261impl RetryExecutor {
262 pub fn new(config: RetryConfig) -> Self {
264 Self {
265 config: Arc::new(config),
266 }
267 }
268
269 pub fn from_shared(config: Arc<RetryConfig>) -> Self {
271 Self { config }
272 }
273
274 pub fn with_defaults() -> Self {
276 Self::new(RetryConfig::default())
277 }
278
279 pub async fn execute<F, Fut, T>(&self, operation: F) -> AptosResult<T>
290 where
291 F: Fn() -> Fut,
292 Fut: Future<Output = AptosResult<T>>,
293 {
294 let mut attempt = 0;
295
296 loop {
297 match operation().await {
298 Ok(result) => return Ok(result),
299 Err(error) => {
300 if attempt >= self.config.max_retries || !self.config.is_retryable_error(&error)
302 {
303 return Err(error);
304 }
305
306 attempt += 1;
307
308 let delay = if let AptosError::RateLimited {
311 retry_after_secs: Some(secs),
312 } = &error
313 {
314 let capped_secs = (*secs).min(300); Duration::from_secs(capped_secs)
317 } else {
318 self.config.delay_for_attempt(attempt)
319 };
320
321 if !delay.is_zero() {
322 sleep(delay).await;
323 }
324 }
325 }
326 }
327 }
328
329 pub async fn execute_with_predicate<F, Fut, T, P>(
336 &self,
337 operation: F,
338 should_retry: P,
339 ) -> AptosResult<T>
340 where
341 F: Fn() -> Fut,
342 Fut: Future<Output = AptosResult<T>>,
343 P: Fn(&AptosError) -> bool,
344 {
345 let mut attempt = 0;
346
347 loop {
348 match operation().await {
349 Ok(result) => return Ok(result),
350 Err(error) => {
351 if attempt >= self.config.max_retries || !should_retry(&error) {
352 return Err(error);
353 }
354
355 attempt += 1;
356
357 let delay = if let AptosError::RateLimited {
359 retry_after_secs: Some(secs),
360 } = &error
361 {
362 let capped_secs = (*secs).min(300);
363 Duration::from_secs(capped_secs)
364 } else {
365 self.config.delay_for_attempt(attempt)
366 };
367
368 if !delay.is_zero() {
369 sleep(delay).await;
370 }
371 }
372 }
373 }
374 }
375}
376
377pub trait RetryExt<T> {
394 fn with_retry(self, config: &RetryConfig) -> impl Future<Output = AptosResult<T>>;
400}
401
402impl<F, Fut, T> RetryExt<T> for F
403where
404 F: Fn() -> Fut,
405 Fut: Future<Output = AptosResult<T>>,
406{
407 fn with_retry(self, config: &RetryConfig) -> impl Future<Output = AptosResult<T>> {
408 let executor = RetryExecutor::new(config.clone());
409 async move { executor.execute(self).await }
410 }
411}
412
413pub async fn retry<F, Fut, T>(operation: F) -> AptosResult<T>
421where
422 F: Fn() -> Fut,
423 Fut: Future<Output = AptosResult<T>>,
424{
425 RetryExecutor::with_defaults().execute(operation).await
426}
427
428pub async fn retry_with_config<F, Fut, T>(config: &RetryConfig, operation: F) -> AptosResult<T>
436where
437 F: Fn() -> Fut,
438 Fut: Future<Output = AptosResult<T>>,
439{
440 RetryExecutor::new(config.clone()).execute(operation).await
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use std::sync::Arc;
447 use std::sync::atomic::{AtomicU32, Ordering};
448
449 #[test]
450 fn test_default_config() {
451 let config = RetryConfig::default();
452 assert_eq!(config.max_retries, 3);
453 assert_eq!(config.initial_delay_ms, 100);
454 assert!(config.jitter);
455 }
456
457 #[test]
458 fn test_no_retry_config() {
459 let config = RetryConfig::no_retry();
460 assert_eq!(config.max_retries, 0);
461 }
462
463 #[test]
464 fn test_builder() {
465 let config = RetryConfig::builder()
466 .max_retries(5)
467 .initial_delay_ms(200)
468 .max_delay_ms(5000)
469 .exponential_base(1.5)
470 .jitter(false)
471 .build();
472
473 assert_eq!(config.max_retries, 5);
474 assert_eq!(config.initial_delay_ms, 200);
475 assert_eq!(config.max_delay_ms, 5000);
476 assert!((config.exponential_base - 1.5).abs() < f64::EPSILON);
477 assert!(!config.jitter);
478 }
479
480 #[test]
481 fn test_delay_calculation_no_jitter() {
482 let config = RetryConfig::builder()
483 .initial_delay_ms(100)
484 .exponential_base(2.0)
485 .jitter(false)
486 .build();
487
488 assert_eq!(config.delay_for_attempt(0), Duration::from_millis(0));
490
491 assert_eq!(config.delay_for_attempt(1), Duration::from_millis(100));
493
494 assert_eq!(config.delay_for_attempt(2), Duration::from_millis(200));
496
497 assert_eq!(config.delay_for_attempt(3), Duration::from_millis(400));
499 }
500
501 #[test]
502 fn test_delay_capped_at_max() {
503 let config = RetryConfig::builder()
504 .initial_delay_ms(1000)
505 .max_delay_ms(2000)
506 .exponential_base(2.0)
507 .jitter(false)
508 .build();
509
510 assert_eq!(config.delay_for_attempt(3), Duration::from_secs(2));
512 }
513
514 #[test]
515 fn test_retryable_status_codes() {
516 let config = RetryConfig::default();
517
518 assert!(config.is_retryable_status(429)); assert!(config.is_retryable_status(503)); assert!(!config.is_retryable_status(400)); assert!(!config.is_retryable_status(404)); }
523
524 #[test]
525 fn test_retryable_errors() {
526 let config = RetryConfig::default();
527
528 let api_error = AptosError::Api {
530 status_code: 503,
531 message: "Service Unavailable".to_string(),
532 error_code: None,
533 vm_error_code: None,
534 };
535 assert!(config.is_retryable_error(&api_error));
536
537 let rate_limited = AptosError::RateLimited {
539 retry_after_secs: Some(30),
540 };
541 assert!(config.is_retryable_error(&rate_limited));
542
543 let api_error_400 = AptosError::Api {
545 status_code: 400,
546 message: "Bad Request".to_string(),
547 error_code: None,
548 vm_error_code: None,
549 };
550 assert!(!config.is_retryable_error(&api_error_400));
551
552 let not_found = AptosError::NotFound("resource".to_string());
554 assert!(!config.is_retryable_error(¬_found));
555 }
556
557 #[tokio::test]
558 async fn test_retry_succeeds_on_first_try() {
559 let executor = RetryExecutor::with_defaults();
560 let counter = Arc::new(AtomicU32::new(0));
561 let counter_clone = counter.clone();
562
563 let result = executor
564 .execute(|| {
565 let counter = counter_clone.clone();
566 async move {
567 counter.fetch_add(1, Ordering::SeqCst);
568 Ok::<_, AptosError>(42)
569 }
570 })
571 .await;
572
573 assert_eq!(result.unwrap(), 42);
574 assert_eq!(counter.load(Ordering::SeqCst), 1);
575 }
576
577 #[tokio::test]
578 async fn test_retry_succeeds_after_failures() {
579 let config = RetryConfig::builder()
580 .max_retries(3)
581 .initial_delay_ms(1) .jitter(false)
583 .build();
584 let executor = RetryExecutor::new(config);
585 let counter = Arc::new(AtomicU32::new(0));
586 let counter_clone = counter.clone();
587
588 let result = executor
589 .execute(|| {
590 let counter = counter_clone.clone();
591 async move {
592 let count = counter.fetch_add(1, Ordering::SeqCst);
593 if count < 2 {
594 Err(AptosError::Api {
595 status_code: 503,
596 message: "Service Unavailable".to_string(),
597 error_code: None,
598 vm_error_code: None,
599 })
600 } else {
601 Ok(42)
602 }
603 }
604 })
605 .await;
606
607 assert_eq!(result.unwrap(), 42);
608 assert_eq!(counter.load(Ordering::SeqCst), 3); }
610
611 #[tokio::test]
612 async fn test_retry_exhausted() {
613 let config = RetryConfig::builder()
614 .max_retries(2)
615 .initial_delay_ms(1)
616 .jitter(false)
617 .build();
618 let executor = RetryExecutor::new(config);
619 let counter = Arc::new(AtomicU32::new(0));
620 let counter_clone = counter.clone();
621
622 let result = executor
623 .execute(|| {
624 let counter = counter_clone.clone();
625 async move {
626 counter.fetch_add(1, Ordering::SeqCst);
627 Err::<i32, _>(AptosError::Api {
628 status_code: 503,
629 message: "Always fails".to_string(),
630 error_code: None,
631 vm_error_code: None,
632 })
633 }
634 })
635 .await;
636
637 assert!(result.is_err());
638 assert_eq!(counter.load(Ordering::SeqCst), 3); }
640
641 #[tokio::test]
642 async fn test_no_retry_on_non_retryable_error() {
643 let config = RetryConfig::builder()
644 .max_retries(3)
645 .initial_delay_ms(1)
646 .build();
647 let executor = RetryExecutor::new(config);
648 let counter = Arc::new(AtomicU32::new(0));
649 let counter_clone = counter.clone();
650
651 let result = executor
652 .execute(|| {
653 let counter = counter_clone.clone();
654 async move {
655 counter.fetch_add(1, Ordering::SeqCst);
656 Err::<i32, _>(AptosError::Api {
657 status_code: 400, message: "Bad Request".to_string(),
659 error_code: None,
660 vm_error_code: None,
661 })
662 }
663 })
664 .await;
665
666 assert!(result.is_err());
667 assert_eq!(counter.load(Ordering::SeqCst), 1); }
669
670 #[test]
671 fn test_aggressive_config() {
672 let config = RetryConfig::aggressive();
673 assert_eq!(config.max_retries, 5);
674 assert_eq!(config.initial_delay_ms, 50);
675 assert_eq!(config.max_delay_ms, 5_000);
676 assert!((config.exponential_base - 1.5).abs() < f64::EPSILON);
677 assert!(config.jitter);
678 }
679
680 #[test]
681 fn test_conservative_config() {
682 let config = RetryConfig::conservative();
683 assert_eq!(config.max_retries, 3);
684 assert_eq!(config.initial_delay_ms, 500);
685 assert_eq!(config.max_delay_ms, 30_000);
686 assert!((config.exponential_base - 2.0).abs() < f64::EPSILON);
687 assert!(config.jitter);
688 }
689
690 #[test]
691 fn test_builder_jitter_factor() {
692 let config = RetryConfig::builder().jitter_factor(0.25).build();
693
694 assert!((config.jitter_factor - 0.25).abs() < f64::EPSILON);
695 }
696
697 #[test]
698 fn test_builder_retryable_status_codes() {
699 let config = RetryConfig::builder()
700 .retryable_status_codes([500, 502])
701 .build();
702
703 assert!(config.is_retryable_status(500));
704 assert!(config.is_retryable_status(502));
705 assert!(!config.is_retryable_status(503)); }
707
708 #[test]
709 fn test_delay_with_jitter() {
710 let config = RetryConfig::builder()
711 .initial_delay_ms(100)
712 .jitter(true)
713 .jitter_factor(0.5)
714 .build();
715
716 let delay1 = config.delay_for_attempt(1);
718 assert!(delay1 >= Duration::from_millis(50));
720 assert!(delay1 <= Duration::from_millis(150));
721 }
722
723 #[test]
724 fn test_delay_zero_for_first_attempt() {
725 let config = RetryConfig::default();
726 assert_eq!(config.delay_for_attempt(0), Duration::from_millis(0));
727 }
728
729 #[test]
730 fn test_retryable_error_transaction_error() {
731 let config = RetryConfig::default();
732
733 let txn_error = AptosError::Transaction("failed".to_string());
735 assert!(!config.is_retryable_error(&txn_error));
736 }
737
738 #[test]
739 fn test_retryable_error_invalid_address() {
740 let config = RetryConfig::default();
741
742 let addr_error = AptosError::InvalidAddress("bad".to_string());
744 assert!(!config.is_retryable_error(&addr_error));
745 }
746
747 #[tokio::test]
748 async fn test_retry_with_no_retry_config() {
749 let config = RetryConfig::no_retry();
750 let executor = RetryExecutor::new(config);
751 let counter = Arc::new(AtomicU32::new(0));
752 let counter_clone = counter.clone();
753
754 let result = executor
755 .execute(|| {
756 let counter = counter_clone.clone();
757 async move {
758 counter.fetch_add(1, Ordering::SeqCst);
759 Err::<i32, _>(AptosError::Api {
760 status_code: 503,
761 message: "Service Unavailable".to_string(),
762 error_code: None,
763 vm_error_code: None,
764 })
765 }
766 })
767 .await;
768
769 assert!(result.is_err());
770 assert_eq!(counter.load(Ordering::SeqCst), 1); }
772
773 #[test]
774 fn test_retry_config_clone() {
775 let config = RetryConfig::builder()
776 .max_retries(5)
777 .initial_delay_ms(200)
778 .build();
779
780 let cloned = config.clone();
781 assert_eq!(config.max_retries, cloned.max_retries);
782 assert_eq!(config.initial_delay_ms, cloned.initial_delay_ms);
783 }
784
785 #[test]
786 fn test_retry_config_debug() {
787 let config = RetryConfig::default();
788 let debug = format!("{config:?}");
789 assert!(debug.contains("RetryConfig"));
790 assert!(debug.contains("max_retries"));
791 }
792
793 #[test]
794 fn test_builder_add_retryable_status_code() {
795 let config = RetryConfig::builder()
796 .add_retryable_status_code(599)
797 .build();
798
799 assert!(config.is_retryable_status(599));
800 }
801
802 #[test]
803 fn test_builder_add_duplicate_status_code() {
804 let config = RetryConfig::builder()
805 .add_retryable_status_code(500)
806 .add_retryable_status_code(500) .build();
808
809 assert!(config.is_retryable_status(500));
811 assert_eq!(config.retryable_status_codes.len(), 1);
813 }
814
815 #[test]
816 fn test_builder_jitter_factor_clamped() {
817 let config = RetryConfig::builder()
818 .jitter_factor(2.0) .build();
820
821 assert!((config.jitter_factor - 1.0).abs() < f64::EPSILON);
822
823 let config2 = RetryConfig::builder()
824 .jitter_factor(-1.0) .build();
826
827 assert!(config2.jitter_factor.abs() < f64::EPSILON);
828 }
829
830 #[test]
831 fn test_retry_executor_new() {
832 let config = RetryConfig::default();
833 let executor = RetryExecutor::new(config.clone());
834
835 let debug = format!("{executor:?}");
836 assert!(debug.contains("RetryExecutor"));
837 }
838
839 #[tokio::test]
840 async fn test_retry_with_custom_predicate() {
841 let config = RetryConfig::builder()
842 .max_retries(3)
843 .initial_delay_ms(1)
844 .jitter(false)
845 .build();
846 let executor = RetryExecutor::new(config);
847 let counter = Arc::new(AtomicU32::new(0));
848 let counter_clone = counter.clone();
849
850 let result = executor
852 .execute_with_predicate(
853 || {
854 let counter = counter_clone.clone();
855 async move {
856 let count = counter.fetch_add(1, Ordering::SeqCst);
857 if count < 2 {
858 Err(AptosError::NotFound("test".to_string()))
859 } else {
860 Ok(42)
861 }
862 }
863 },
864 |_| true, )
866 .await;
867
868 assert_eq!(result.unwrap(), 42);
869 assert_eq!(counter.load(Ordering::SeqCst), 3);
870 }
871
872 #[tokio::test]
873 async fn test_retry_with_predicate_no_retry() {
874 let config = RetryConfig::builder()
875 .max_retries(3)
876 .initial_delay_ms(1)
877 .build();
878 let executor = RetryExecutor::new(config);
879 let counter = Arc::new(AtomicU32::new(0));
880 let counter_clone = counter.clone();
881
882 let result = executor
884 .execute_with_predicate(
885 || {
886 let counter = counter_clone.clone();
887 async move {
888 counter.fetch_add(1, Ordering::SeqCst);
889 Err::<i32, _>(AptosError::Api {
890 status_code: 503,
891 message: "Fail".to_string(),
892 error_code: None,
893 vm_error_code: None,
894 })
895 }
896 },
897 |_| false, )
899 .await;
900
901 assert!(result.is_err());
902 assert_eq!(counter.load(Ordering::SeqCst), 1); }
904
905 #[tokio::test]
906 async fn test_retry_convenience_function() {
907 let counter = Arc::new(AtomicU32::new(0));
908 let counter_clone = counter.clone();
909
910 let result = retry(|| {
911 let counter = counter_clone.clone();
912 async move {
913 counter.fetch_add(1, Ordering::SeqCst);
914 Ok::<_, AptosError>(42)
915 }
916 })
917 .await;
918
919 assert_eq!(result.unwrap(), 42);
920 assert_eq!(counter.load(Ordering::SeqCst), 1);
921 }
922
923 #[tokio::test]
924 async fn test_retry_with_config_convenience_function() {
925 let config = RetryConfig::builder()
926 .max_retries(1)
927 .initial_delay_ms(1)
928 .jitter(false)
929 .build();
930 let counter = Arc::new(AtomicU32::new(0));
931 let counter_clone = counter.clone();
932
933 let result = retry_with_config(&config, || {
934 let counter = counter_clone.clone();
935 async move {
936 let count = counter.fetch_add(1, Ordering::SeqCst);
937 if count < 1 {
938 Err(AptosError::Api {
940 status_code: 503,
941 message: "Service Unavailable".to_string(),
942 error_code: None,
943 vm_error_code: None,
944 })
945 } else {
946 Ok(42)
947 }
948 }
949 })
950 .await;
951
952 assert_eq!(result.unwrap(), 42);
953 assert_eq!(counter.load(Ordering::SeqCst), 2);
954 }
955
956 #[test]
957 fn test_retryable_rate_limited_error() {
958 let config = RetryConfig::default();
959
960 let rate_limited = AptosError::RateLimited {
962 retry_after_secs: Some(5),
963 };
964 assert!(config.is_retryable_error(&rate_limited));
965 }
966
967 #[test]
968 fn test_builder_default_debug() {
969 let builder = RetryConfigBuilder::default();
970 let debug = format!("{builder:?}");
971 assert!(debug.contains("RetryConfigBuilder"));
972 }
973
974 #[tokio::test]
975 async fn test_retry_ext_with_retry_succeeds_after_failures() {
976 let config = RetryConfig::builder()
977 .max_retries(3)
978 .initial_delay_ms(1)
979 .jitter(false)
980 .build();
981 let counter = Arc::new(AtomicU32::new(0));
982 let counter_clone = counter.clone();
983
984 let result = (move || {
986 let counter = counter_clone.clone();
987 async move {
988 let count = counter.fetch_add(1, Ordering::SeqCst);
989 if count < 2 {
990 Err(AptosError::Api {
991 status_code: 503,
992 message: "Service Unavailable".to_string(),
993 error_code: None,
994 vm_error_code: None,
995 })
996 } else {
997 Ok(42)
998 }
999 }
1000 })
1001 .with_retry(&config)
1002 .await;
1003
1004 assert_eq!(result.unwrap(), 42);
1005 assert_eq!(counter.load(Ordering::SeqCst), 3); }
1007
1008 #[tokio::test]
1009 async fn test_retry_ext_with_retry_no_retry_on_non_retryable() {
1010 let config = RetryConfig::builder()
1011 .max_retries(3)
1012 .initial_delay_ms(1)
1013 .jitter(false)
1014 .build();
1015 let counter = Arc::new(AtomicU32::new(0));
1016 let counter_clone = counter.clone();
1017
1018 let result: AptosResult<i32> = (move || {
1019 let counter = counter_clone.clone();
1020 async move {
1021 counter.fetch_add(1, Ordering::SeqCst);
1022 Err(AptosError::Api {
1023 status_code: 400, message: "Bad Request".to_string(),
1025 error_code: None,
1026 vm_error_code: None,
1027 })
1028 }
1029 })
1030 .with_retry(&config)
1031 .await;
1032
1033 assert!(result.is_err());
1034 assert_eq!(counter.load(Ordering::SeqCst), 1); }
1036}