1use std::sync::{Mutex, OnceLock};
7use std::time::{Duration, SystemTime};
8
9use tokio::sync::OwnedSemaphorePermit;
10use tracing::{debug, trace};
11
12use aws_smithy_runtime_api::box_error::BoxError;
13use aws_smithy_runtime_api::client::interceptors::context::{
14 BeforeTransmitInterceptorContextMut, InterceptorContext,
15};
16use aws_smithy_runtime_api::client::interceptors::{dyn_dispatch_hint, Intercept};
17use aws_smithy_runtime_api::client::retries::classifiers::{RetryAction, RetryReason};
18use aws_smithy_runtime_api::client::retries::{RequestAttempts, RetryStrategy, ShouldAttempt};
19use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
20use aws_smithy_types::config_bag::{ConfigBag, Layer, Storable, StoreReplace};
21use aws_smithy_types::retry::{ErrorKind, RetryConfig, RetryMode, RetrySpec};
22
23use crate::client::retries::classifiers::run_classifiers_on_ctx;
24use crate::client::retries::client_rate_limiter::{ClientRateLimiter, RequestReason};
25use crate::client::retries::strategy::standard::ReleaseResult::{
26 APermitWasReleased, NoPermitWasReleased,
27};
28use crate::client::retries::token_bucket::TokenBucket;
29use crate::client::retries::{
30 ClientRateLimiterPartition, LongPollingBackoff, RetryPartition, RetryPartitionInner,
31};
32use crate::static_partition_map::StaticPartitionMap;
33
34static CLIENT_RATE_LIMITER: StaticPartitionMap<ClientRateLimiterPartition, ClientRateLimiter> =
35 StaticPartitionMap::new();
36
37static TOKEN_BUCKET: StaticPartitionMap<RetryPartition, TokenBucket> = StaticPartitionMap::new();
39
40#[derive(Debug, Default)]
42pub struct StandardRetryStrategy {
43 retry_permit: Mutex<Option<OwnedSemaphorePermit>>,
44}
45
46impl Storable for StandardRetryStrategy {
47 type Storer = StoreReplace<Self>;
48}
49
50impl StandardRetryStrategy {
51 pub fn new() -> Self {
53 Default::default()
54 }
55
56 fn release_retry_permit(&self, token_bucket: &TokenBucket) -> ReleaseResult {
57 let mut retry_permit = self.retry_permit.lock().unwrap();
58 match retry_permit.take() {
59 Some(p) => {
60 if token_bucket.success_reward() > 0.0 {
62 token_bucket.reward_success();
63 p.forget();
64 } else {
65 drop(p); }
67 APermitWasReleased
68 }
69 None => {
70 if token_bucket.success_reward() > 0.0 {
72 token_bucket.reward_success();
73 } else {
74 token_bucket.regenerate_a_token();
75 }
76 NoPermitWasReleased
77 }
78 }
79 }
80
81 fn set_retry_permit(&self, new_retry_permit: OwnedSemaphorePermit) {
82 let mut old_retry_permit = self.retry_permit.lock().unwrap();
83 if let Some(p) = old_retry_permit.replace(new_retry_permit) {
84 p.forget()
87 }
88 }
89
90 fn adaptive_retry_rate_limiter(
92 runtime_components: &RuntimeComponents,
93 cfg: &ConfigBag,
94 ) -> Option<ClientRateLimiter> {
95 let retry_config = cfg.load::<RetryConfig>().expect("retry config is required");
96 if retry_config.mode() == RetryMode::Adaptive {
97 if let Some(time_source) = runtime_components.time_source() {
98 let retry_partition = cfg.load::<RetryPartition>().expect("set in default config");
99 let seconds_since_unix_epoch = time_source
100 .now()
101 .duration_since(SystemTime::UNIX_EPOCH)
102 .expect("the present takes place after the UNIX_EPOCH")
103 .as_secs_f64();
104 let client_rate_limiter = match &retry_partition.inner {
105 RetryPartitionInner::Default(_) => {
106 let client_rate_limiter_partition =
107 ClientRateLimiterPartition::new(retry_partition.clone());
108 CLIENT_RATE_LIMITER.get_or_init(client_rate_limiter_partition, || {
109 ClientRateLimiter::new(seconds_since_unix_epoch)
110 })
111 }
112 RetryPartitionInner::Custom {
113 client_rate_limiter,
114 ..
115 } => client_rate_limiter.clone(),
116 };
117 return Some(client_rate_limiter);
118 }
119 }
120 None
121 }
122
123 fn calculate_backoff(
124 &self,
125 runtime_components: &RuntimeComponents,
126 cfg: &ConfigBag,
127 retry_cfg: &RetryConfig,
128 retry_reason: &RetryAction,
129 ) -> Result<Duration, ShouldAttempt> {
130 let request_attempts = cfg
131 .load::<RequestAttempts>()
132 .expect("at least one request attempt is made before any retry is attempted")
133 .attempts();
134
135 match retry_reason {
136 RetryAction::RetryIndicated(RetryReason::RetryableError { kind, retry_after }) => {
137 let initial_backoff = if *kind != ErrorKind::ThrottlingError {
138 retry_cfg
139 .retry_spec()
140 .map(|s| s.non_throttling_initial_backoff())
141 .unwrap_or(retry_cfg.initial_backoff())
142 .as_secs_f64()
143 } else {
144 retry_cfg.initial_backoff().as_secs_f64()
145 };
146
147 if let Some(delay) = check_rate_limiter_for_delay(runtime_components, cfg, *kind) {
148 let delay = delay.min(retry_cfg.max_backoff());
149 debug!("rate limiter has requested a {delay:?} delay before retrying");
150 Ok(delay)
151 } else {
152 let base = if retry_cfg.use_static_exponential_base() {
153 1.0
154 } else {
155 fastrand::f64()
156 };
157 let t_i = calculate_exponential_backoff(
158 base,
159 initial_backoff,
160 request_attempts - 1,
161 retry_cfg.max_backoff(),
162 );
163
164 if let Some(retry_after) = *retry_after {
165 if retry_cfg
166 .retry_spec()
167 .is_some_and(|s| s.is_at_least(RetrySpec::V2_1))
168 {
169 let delay = retry_after.clamp(t_i, t_i + Duration::from_secs(5));
170 debug!("x-amz-retry-after bounded to {delay:?} (t_i={t_i:?})");
171 Ok(delay)
172 } else {
173 let delay = retry_after.min(retry_cfg.max_backoff());
174 debug!(
175 "explicit request from server to delay {delay:?} before retrying"
176 );
177 Ok(delay)
178 }
179 } else {
180 Ok(t_i)
181 }
182 }
183 }
184 RetryAction::RetryForbidden | RetryAction::NoActionIndicated => {
185 debug!(
186 attempts = request_attempts,
187 max_attempts = retry_cfg.max_attempts(),
188 "encountered un-retryable error"
189 );
190 Err(ShouldAttempt::No)
191 }
192 _ => unreachable!("RetryAction is non-exhaustive"),
193 }
194 }
195}
196
197enum ReleaseResult {
198 APermitWasReleased,
199 NoPermitWasReleased,
200}
201
202impl RetryStrategy for StandardRetryStrategy {
203 fn should_attempt_initial_request(
204 &self,
205 runtime_components: &RuntimeComponents,
206 cfg: &ConfigBag,
207 ) -> Result<ShouldAttempt, BoxError> {
208 if let Some(crl) = Self::adaptive_retry_rate_limiter(runtime_components, cfg) {
209 let seconds_since_unix_epoch = get_seconds_since_unix_epoch(runtime_components);
210 if let Err(delay) = crl.acquire_permission_to_send_a_request(
211 seconds_since_unix_epoch,
212 RequestReason::InitialRequest,
213 ) {
214 return Ok(ShouldAttempt::YesAfterDelay(delay));
215 }
216 } else {
217 debug!("no client rate limiter configured, so no token is required for the initial request.");
218 }
219
220 Ok(ShouldAttempt::Yes)
221 }
222
223 fn should_attempt_retry(
224 &self,
225 ctx: &InterceptorContext,
226 runtime_components: &RuntimeComponents,
227 cfg: &ConfigBag,
228 ) -> Result<ShouldAttempt, BoxError> {
229 let retry_cfg = cfg.load::<RetryConfig>().expect("retry config is required");
230
231 let token_bucket = cfg.load::<TokenBucket>().expect("token bucket is required");
233 let retry_classifiers = runtime_components.retry_classifiers();
235 let classifier_result = run_classifiers_on_ctx(retry_classifiers, ctx);
236
237 let error_kind = error_kind(&classifier_result);
242 let is_throttling_error = error_kind
243 .map(|kind| kind == ErrorKind::ThrottlingError)
244 .unwrap_or(false);
245 update_rate_limiter_if_exists(runtime_components, cfg, is_throttling_error);
246
247 if !ctx.is_failed() {
249 self.release_retry_permit(token_bucket);
250 }
251 let request_attempts = cfg
254 .load::<RequestAttempts>()
255 .expect("at least one request attempt is made before any retry is attempted")
256 .attempts();
257
258 if !classifier_result.should_retry() {
260 debug!(
261 "attempt #{request_attempts} classified as {:?}, not retrying",
262 classifier_result
263 );
264 return Ok(ShouldAttempt::No);
265 }
266
267 if request_attempts >= retry_cfg.max_attempts() {
269 debug!(
270 attempts = request_attempts,
271 max_attempts = retry_cfg.max_attempts(),
272 "not retrying because we are out of attempts"
273 );
274 return Ok(ShouldAttempt::No);
275 }
276
277 let error_kind = error_kind.expect("result was classified retryable");
279 let is_long_polling = retry_cfg.retry_spec().is_some_and(|s| s.long_polling());
280
281 let backoff =
284 match self.calculate_backoff(runtime_components, cfg, retry_cfg, &classifier_result) {
285 Ok(value) => value,
286 Err(value) => return Ok(value),
287 };
288
289 match token_bucket.acquire(
291 &error_kind,
292 &runtime_components.time_source().unwrap_or_default(),
293 ) {
294 Some(permit) => self.set_retry_permit(permit),
295 None => {
296 debug!("attempt #{request_attempts} failed with {error_kind:?}; not enough retry quota.");
297 if is_long_polling {
298 if let Some(hint) = cfg.load::<LongPollingBackoff>() {
299 hint.set(backoff);
300 }
301 }
302 return Ok(ShouldAttempt::No);
303 }
304 }
305
306 debug!(
307 "attempt #{request_attempts} failed with {:?}; retrying after {:?}",
308 classifier_result, backoff
309 );
310 Ok(ShouldAttempt::YesAfterDelay(backoff))
311 }
312}
313
314fn error_kind(classifier_result: &RetryAction) -> Option<ErrorKind> {
316 match classifier_result {
317 RetryAction::RetryIndicated(RetryReason::RetryableError { kind, .. }) => Some(*kind),
318 _ => None,
319 }
320}
321
322fn update_rate_limiter_if_exists(
323 runtime_components: &RuntimeComponents,
324 cfg: &ConfigBag,
325 is_throttling_error: bool,
326) {
327 if let Some(crl) = StandardRetryStrategy::adaptive_retry_rate_limiter(runtime_components, cfg) {
328 let seconds_since_unix_epoch = get_seconds_since_unix_epoch(runtime_components);
329 crl.update_rate_limiter(seconds_since_unix_epoch, is_throttling_error);
330 }
331}
332
333fn check_rate_limiter_for_delay(
334 runtime_components: &RuntimeComponents,
335 cfg: &ConfigBag,
336 kind: ErrorKind,
337) -> Option<Duration> {
338 if let Some(crl) = StandardRetryStrategy::adaptive_retry_rate_limiter(runtime_components, cfg) {
339 let is_v2_1 = cfg
347 .load::<RetryConfig>()
348 .and_then(|rc| rc.retry_spec())
349 .is_some_and(|s| s.is_at_least(RetrySpec::V2_1));
350 if is_v2_1 {
351 return None;
352 }
353 let retry_reason = if kind == ErrorKind::ThrottlingError {
354 RequestReason::RetryTimeout
355 } else {
356 RequestReason::Retry
357 };
358 if let Err(delay) = crl.acquire_permission_to_send_a_request(
359 get_seconds_since_unix_epoch(runtime_components),
360 retry_reason,
361 ) {
362 return Some(delay);
363 }
364 }
365
366 None
367}
368
369pub(super) fn calculate_exponential_backoff(
370 base: f64,
371 initial_backoff: f64,
372 retry_attempts: u32,
373 max_backoff: Duration,
374) -> Duration {
375 let result = match 2_u32
376 .checked_pow(retry_attempts)
377 .map(|power| (power as f64) * initial_backoff)
378 {
379 Some(backoff) => match Duration::try_from_secs_f64(backoff) {
380 Ok(result) => result.min(max_backoff),
381 Err(e) => {
382 tracing::warn!("falling back to {max_backoff:?} as `Duration` could not be created for exponential backoff: {e}");
383 max_backoff
384 }
385 },
386 None => max_backoff,
387 };
388
389 result.mul_f64(base)
392}
393
394pub(super) fn get_seconds_since_unix_epoch(runtime_components: &RuntimeComponents) -> f64 {
395 let request_time = runtime_components
396 .time_source()
397 .expect("time source required for retries");
398 request_time
399 .now()
400 .duration_since(SystemTime::UNIX_EPOCH)
401 .unwrap()
402 .as_secs_f64()
403}
404
405#[derive(Debug)]
409pub(crate) struct TokenBucketProvider {
410 default_partition: RetryPartition,
411 token_bucket: OnceLock<TokenBucket>,
412}
413
414impl TokenBucketProvider {
415 pub(crate) fn new(default_partition: RetryPartition) -> Self {
420 Self {
421 default_partition,
422 token_bucket: OnceLock::new(),
423 }
424 }
425}
426
427fn token_bucket_for_spec(cfg: &ConfigBag) -> TokenBucket {
429 let is_v2_1 = cfg
430 .load::<RetryConfig>()
431 .and_then(|rc| rc.retry_spec())
432 .is_some_and(|s| s.is_at_least(RetrySpec::V2_1));
433 if is_v2_1 {
434 TokenBucket::builder()
435 .retry_cost(14)
436 .throttling_retry_cost(5)
437 .timeout_retry_cost(14)
438 .build()
439 } else {
440 TokenBucket::default()
441 }
442}
443
444#[dyn_dispatch_hint]
445impl Intercept for TokenBucketProvider {
446 fn name(&self) -> &'static str {
447 "TokenBucketProvider"
448 }
449
450 fn modify_before_retry_loop(
451 &self,
452 _context: &mut BeforeTransmitInterceptorContextMut<'_>,
453 _runtime_components: &RuntimeComponents,
454 cfg: &mut ConfigBag,
455 ) -> Result<(), BoxError> {
456 let retry_partition = cfg.load::<RetryPartition>().expect("set in default config");
457
458 let tb = match &retry_partition.inner {
459 RetryPartitionInner::Default(name) => {
460 if name == self.default_partition.name() {
461 self.token_bucket
462 .get_or_init(|| {
463 TOKEN_BUCKET.get_or_init(self.default_partition.clone(), || {
464 token_bucket_for_spec(cfg)
465 })
466 })
467 .clone()
468 } else {
469 TOKEN_BUCKET.get_or_init(retry_partition.clone(), || token_bucket_for_spec(cfg))
470 }
471 }
472 RetryPartitionInner::Custom { token_bucket, .. } => token_bucket.clone(),
473 };
474
475 trace!("token bucket for {retry_partition:?} added to config bag");
476 let mut layer = Layer::new("token_bucket_partition");
477 layer.store_put(tb);
478 cfg.push_layer(layer);
479 Ok(())
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 #[allow(unused_imports)] use std::fmt;
487 use std::sync::Mutex;
488 use std::time::Duration;
489
490 use aws_smithy_async::time::SystemTimeSource;
491 use aws_smithy_runtime_api::client::interceptors::context::{
492 Input, InterceptorContext, Output,
493 };
494 use aws_smithy_runtime_api::client::orchestrator::OrchestratorError;
495 use aws_smithy_runtime_api::client::retries::classifiers::{
496 ClassifyRetry, RetryAction, SharedRetryClassifier,
497 };
498 use aws_smithy_runtime_api::client::retries::{
499 AlwaysRetry, RequestAttempts, RetryStrategy, ShouldAttempt,
500 };
501 use aws_smithy_runtime_api::client::runtime_components::{
502 RuntimeComponents, RuntimeComponentsBuilder,
503 };
504 use aws_smithy_types::config_bag::{ConfigBag, Layer};
505 use aws_smithy_types::retry::{ErrorKind, RetryConfig};
506
507 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
508 use aws_smithy_types::retry::RetrySpec;
509
510 use super::{calculate_exponential_backoff, StandardRetryStrategy};
511 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
512 use crate::client::retries::token_bucket::{
513 DEFAULT_CAPACITY, DEFAULT_RETRY_COST, DEFAULT_RETRY_TIMEOUT_COST, THROTTLING_RETRY_COST,
514 };
515 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
516 use crate::client::retries::LongPollingBackoff;
517 use crate::client::retries::{ClientRateLimiter, RetryPartition, TokenBucket};
518
519 #[test]
520 fn no_retry_necessary_for_ok_result() {
521 let cfg = ConfigBag::of_layers(vec![{
522 let mut layer = Layer::new("test");
523 layer.store_put(RetryConfig::standard());
524 layer.store_put(RequestAttempts::new(1));
525 layer.store_put(TokenBucket::default());
526 layer
527 }]);
528 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
529 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
530 let strategy = StandardRetryStrategy::default();
531 ctx.set_output_or_error(Ok(Output::doesnt_matter()));
532
533 let actual = strategy
534 .should_attempt_retry(&ctx, &rc, &cfg)
535 .expect("method is infallible for this use");
536 assert_eq!(ShouldAttempt::No, actual);
537 }
538
539 fn set_up_cfg_and_context(
540 error_kind: ErrorKind,
541 current_request_attempts: u32,
542 retry_config: RetryConfig,
543 ) -> (InterceptorContext, RuntimeComponents, ConfigBag) {
544 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
545 ctx.set_output_or_error(Err(OrchestratorError::other("doesn't matter")));
546 let rc = RuntimeComponentsBuilder::for_tests()
547 .with_retry_classifier(SharedRetryClassifier::new(AlwaysRetry(error_kind)))
548 .build()
549 .unwrap();
550 let mut layer = Layer::new("test");
551 layer.store_put(RequestAttempts::new(current_request_attempts));
552 layer.store_put(retry_config);
553 layer.store_put(TokenBucket::default());
554 let cfg = ConfigBag::of_layers(vec![layer]);
555
556 (ctx, rc, cfg)
557 }
558
559 fn test_should_retry_error_kind(error_kind: ErrorKind) {
562 let (ctx, rc, cfg) = set_up_cfg_and_context(
563 error_kind,
564 3,
565 RetryConfig::standard()
566 .with_use_static_exponential_base(true)
567 .with_max_attempts(4),
568 );
569 let strategy = StandardRetryStrategy::new();
570 let actual = strategy
571 .should_attempt_retry(&ctx, &rc, &cfg)
572 .expect("method is infallible for this use");
573 assert_eq!(ShouldAttempt::YesAfterDelay(Duration::from_secs(4)), actual);
574 }
575
576 #[test]
577 fn should_retry_transient_error_result_after_2s() {
578 test_should_retry_error_kind(ErrorKind::TransientError);
579 }
580
581 #[test]
582 fn should_retry_client_error_result_after_2s() {
583 test_should_retry_error_kind(ErrorKind::ClientError);
584 }
585
586 #[test]
587 fn should_retry_server_error_result_after_2s() {
588 test_should_retry_error_kind(ErrorKind::ServerError);
589 }
590
591 #[test]
592 fn should_retry_throttling_error_result_after_2s() {
593 test_should_retry_error_kind(ErrorKind::ThrottlingError);
594 }
595
596 #[test]
597 fn dont_retry_when_out_of_attempts() {
598 let current_attempts = 4;
599 let max_attempts = current_attempts;
600 let (ctx, rc, cfg) = set_up_cfg_and_context(
601 ErrorKind::TransientError,
602 current_attempts,
603 RetryConfig::standard()
604 .with_use_static_exponential_base(true)
605 .with_max_attempts(max_attempts),
606 );
607 let strategy = StandardRetryStrategy::new();
608 let actual = strategy
609 .should_attempt_retry(&ctx, &rc, &cfg)
610 .expect("method is infallible for this use");
611 assert_eq!(ShouldAttempt::No, actual);
612 }
613
614 #[test]
615 fn should_not_panic_when_exponential_backoff_duration_could_not_be_created() {
616 let (ctx, rc, cfg) = set_up_cfg_and_context(
617 ErrorKind::TransientError,
618 33,
620 RetryConfig::standard()
621 .with_use_static_exponential_base(true)
622 .with_max_attempts(100), );
624 let strategy = StandardRetryStrategy::new();
625 let actual = strategy
626 .should_attempt_retry(&ctx, &rc, &cfg)
627 .expect("method is infallible for this use");
628 assert_eq!(ShouldAttempt::YesAfterDelay(MAX_BACKOFF), actual);
629 }
630
631 #[test]
632 fn should_yield_client_rate_limiter_from_custom_partition() {
633 let expected = ClientRateLimiter::builder().token_refill_rate(3.14).build();
634 let cfg = ConfigBag::of_layers(vec![
635 {
637 let mut layer = Layer::new("default");
638 layer.store_put(RetryPartition::new("default"));
639 layer
640 },
641 {
642 let mut layer = Layer::new("user");
643 layer.store_put(RetryConfig::adaptive());
644 layer.store_put(
645 RetryPartition::custom("user")
646 .client_rate_limiter(expected.clone())
647 .build(),
648 );
649 layer
650 },
651 ]);
652 let rc = RuntimeComponentsBuilder::for_tests()
653 .with_time_source(Some(SystemTimeSource::new()))
654 .build()
655 .unwrap();
656 let actual = StandardRetryStrategy::adaptive_retry_rate_limiter(&rc, &cfg)
657 .expect("should yield client rate limiter from custom partition");
658 assert!(std::sync::Arc::ptr_eq(&expected.inner, &actual.inner));
659 }
660
661 #[allow(dead_code)] #[derive(Debug)]
663 struct PresetReasonRetryClassifier {
664 retry_actions: Mutex<Vec<RetryAction>>,
665 }
666
667 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
668 impl PresetReasonRetryClassifier {
669 fn new(mut retry_reasons: Vec<RetryAction>) -> Self {
670 retry_reasons.reverse();
672 Self {
673 retry_actions: Mutex::new(retry_reasons),
674 }
675 }
676 }
677
678 impl ClassifyRetry for PresetReasonRetryClassifier {
679 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
680 let output_or_error = ctx.output_or_error();
682 match output_or_error {
684 Some(Ok(_)) | None => return RetryAction::NoActionIndicated,
685 _ => (),
686 };
687
688 let mut retry_actions = self.retry_actions.lock().unwrap();
689 if retry_actions.len() == 1 {
690 retry_actions.first().unwrap().clone()
691 } else {
692 retry_actions.pop().unwrap()
693 }
694 }
695
696 fn name(&self) -> &'static str {
697 "Always returns a preset retry reason"
698 }
699 }
700
701 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
702 fn setup_test(
703 retry_reasons: Vec<RetryAction>,
704 retry_config: RetryConfig,
705 ) -> (ConfigBag, RuntimeComponents, InterceptorContext) {
706 let rc = RuntimeComponentsBuilder::for_tests()
707 .with_retry_classifier(SharedRetryClassifier::new(
708 PresetReasonRetryClassifier::new(retry_reasons),
709 ))
710 .build()
711 .unwrap();
712 let mut layer = Layer::new("test");
713 layer.store_put(retry_config);
714 let cfg = ConfigBag::of_layers(vec![layer]);
715 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
716 ctx.set_output_or_error(Err(OrchestratorError::other("doesn't matter")));
718
719 (cfg, rc, ctx)
720 }
721
722 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
723 #[test]
724 fn eventual_success() {
725 let (mut cfg, rc, mut ctx) = setup_test(
726 vec![RetryAction::server_error()],
727 RetryConfig::standard()
728 .with_use_static_exponential_base(true)
729 .with_max_attempts(5),
730 );
731 let strategy = StandardRetryStrategy::new();
732 cfg.interceptor_state().store_put(TokenBucket::default());
733 let token_bucket = cfg.load::<TokenBucket>().unwrap().clone();
734
735 cfg.interceptor_state().store_put(RequestAttempts::new(1));
736 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
737 let dur = should_retry.expect_delay();
738 assert_eq!(dur, Duration::from_secs(1));
739 assert_eq!(token_bucket.available_permits(), 495);
740
741 cfg.interceptor_state().store_put(RequestAttempts::new(2));
742 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
743 let dur = should_retry.expect_delay();
744 assert_eq!(dur, Duration::from_secs(2));
745 assert_eq!(token_bucket.available_permits(), 490);
746
747 ctx.set_output_or_error(Ok(Output::doesnt_matter()));
748
749 cfg.interceptor_state().store_put(RequestAttempts::new(3));
750 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
751 assert_eq!(no_retry, ShouldAttempt::No);
752 assert_eq!(token_bucket.available_permits(), 495);
753 }
754
755 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
756 #[test]
757 fn no_more_attempts() {
758 let (mut cfg, rc, ctx) = setup_test(
759 vec![RetryAction::server_error()],
760 RetryConfig::standard()
761 .with_use_static_exponential_base(true)
762 .with_max_attempts(3),
763 );
764 let strategy = StandardRetryStrategy::new();
765 cfg.interceptor_state().store_put(TokenBucket::default());
766 let token_bucket = cfg.load::<TokenBucket>().unwrap().clone();
767
768 cfg.interceptor_state().store_put(RequestAttempts::new(1));
769 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
770 let dur = should_retry.expect_delay();
771 assert_eq!(dur, Duration::from_secs(1));
772 assert_eq!(token_bucket.available_permits(), 495);
773
774 cfg.interceptor_state().store_put(RequestAttempts::new(2));
775 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
776 let dur = should_retry.expect_delay();
777 assert_eq!(dur, Duration::from_secs(2));
778 assert_eq!(token_bucket.available_permits(), 490);
779
780 cfg.interceptor_state().store_put(RequestAttempts::new(3));
781 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
782 assert_eq!(no_retry, ShouldAttempt::No);
783 assert_eq!(token_bucket.available_permits(), 490);
784 }
785
786 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
787 #[test]
788 fn successful_request_and_deser_should_be_retryable() {
789 #[derive(Clone, Copy, Debug)]
790 enum LongRunningOperationStatus {
791 Running,
792 Complete,
793 }
794
795 #[derive(Debug)]
796 struct LongRunningOperationOutput {
797 status: Option<LongRunningOperationStatus>,
798 }
799
800 impl LongRunningOperationOutput {
801 fn status(&self) -> Option<LongRunningOperationStatus> {
802 self.status
803 }
804 }
805
806 struct WaiterRetryClassifier {}
807
808 impl WaiterRetryClassifier {
809 fn new() -> Self {
810 WaiterRetryClassifier {}
811 }
812 }
813
814 impl fmt::Debug for WaiterRetryClassifier {
815 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
816 write!(f, "WaiterRetryClassifier")
817 }
818 }
819 impl ClassifyRetry for WaiterRetryClassifier {
820 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
821 let status: Option<LongRunningOperationStatus> =
822 ctx.output_or_error().and_then(|res| {
823 res.ok().and_then(|output| {
824 output
825 .downcast_ref::<LongRunningOperationOutput>()
826 .and_then(|output| output.status())
827 })
828 });
829
830 if let Some(LongRunningOperationStatus::Running) = status {
831 return RetryAction::server_error();
832 };
833
834 RetryAction::NoActionIndicated
835 }
836
837 fn name(&self) -> &'static str {
838 "waiter retry classifier"
839 }
840 }
841
842 let retry_config = RetryConfig::standard()
843 .with_use_static_exponential_base(true)
844 .with_max_attempts(5);
845
846 let rc = RuntimeComponentsBuilder::for_tests()
847 .with_retry_classifier(SharedRetryClassifier::new(WaiterRetryClassifier::new()))
848 .build()
849 .unwrap();
850 let mut layer = Layer::new("test");
851 layer.store_put(retry_config);
852 let mut cfg = ConfigBag::of_layers(vec![layer]);
853 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
854 let strategy = StandardRetryStrategy::new();
855
856 ctx.set_output_or_error(Ok(Output::erase(LongRunningOperationOutput {
857 status: Some(LongRunningOperationStatus::Running),
858 })));
859
860 cfg.interceptor_state().store_put(TokenBucket::new(5));
861 let token_bucket = cfg.load::<TokenBucket>().unwrap().clone();
862
863 cfg.interceptor_state().store_put(RequestAttempts::new(1));
864 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
865 let dur = should_retry.expect_delay();
866 assert_eq!(dur, Duration::from_secs(1));
867 assert_eq!(token_bucket.available_permits(), 0);
868
869 ctx.set_output_or_error(Ok(Output::erase(LongRunningOperationOutput {
870 status: Some(LongRunningOperationStatus::Complete),
871 })));
872 cfg.interceptor_state().store_put(RequestAttempts::new(2));
873 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
874 should_retry.expect_no();
875 assert_eq!(token_bucket.available_permits(), 5);
876 }
877
878 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
879 #[test]
880 fn no_quota() {
881 let (mut cfg, rc, ctx) = setup_test(
882 vec![RetryAction::server_error()],
883 RetryConfig::standard()
884 .with_use_static_exponential_base(true)
885 .with_max_attempts(5),
886 );
887 let strategy = StandardRetryStrategy::new();
888 cfg.interceptor_state().store_put(TokenBucket::new(5));
889 let token_bucket = cfg.load::<TokenBucket>().unwrap().clone();
890
891 cfg.interceptor_state().store_put(RequestAttempts::new(1));
892 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
893 let dur = should_retry.expect_delay();
894 assert_eq!(dur, Duration::from_secs(1));
895 assert_eq!(token_bucket.available_permits(), 0);
896
897 cfg.interceptor_state().store_put(RequestAttempts::new(2));
898 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
899 assert_eq!(no_retry, ShouldAttempt::No);
900 assert_eq!(token_bucket.available_permits(), 0);
901 }
902
903 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
904 #[test]
905 fn quota_replenishes_on_success() {
906 let (mut cfg, rc, mut ctx) = setup_test(
907 vec![
908 RetryAction::transient_error(),
909 RetryAction::retryable_error_with_explicit_delay(
910 ErrorKind::TransientError,
911 Duration::from_secs(1),
912 ),
913 ],
914 RetryConfig::standard()
915 .with_use_static_exponential_base(true)
916 .with_max_attempts(5),
917 );
918 let strategy = StandardRetryStrategy::new();
919 cfg.interceptor_state().store_put(TokenBucket::new(100));
920 let token_bucket = cfg.load::<TokenBucket>().unwrap().clone();
921
922 cfg.interceptor_state().store_put(RequestAttempts::new(1));
923 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
924 let dur = should_retry.expect_delay();
925 assert_eq!(dur, Duration::from_secs(1));
926 assert_eq!(token_bucket.available_permits(), 90);
927
928 cfg.interceptor_state().store_put(RequestAttempts::new(2));
929 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
930 let dur = should_retry.expect_delay();
931 assert_eq!(dur, Duration::from_secs(1));
932 assert_eq!(token_bucket.available_permits(), 80);
933
934 ctx.set_output_or_error(Ok(Output::doesnt_matter()));
935
936 cfg.interceptor_state().store_put(RequestAttempts::new(3));
937 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
938 assert_eq!(no_retry, ShouldAttempt::No);
939
940 assert_eq!(token_bucket.available_permits(), 90);
941 }
942
943 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
944 #[test]
945 fn quota_replenishes_on_first_try_success() {
946 const PERMIT_COUNT: usize = 20;
947 let (mut cfg, rc, mut ctx) = setup_test(
948 vec![RetryAction::transient_error()],
949 RetryConfig::standard()
950 .with_use_static_exponential_base(true)
951 .with_max_attempts(u32::MAX),
952 );
953 let strategy = StandardRetryStrategy::new();
954 cfg.interceptor_state()
955 .store_put(TokenBucket::new(PERMIT_COUNT));
956 let token_bucket = cfg.load::<TokenBucket>().unwrap().clone();
957
958 let mut attempt = 1;
959
960 while token_bucket.available_permits() > 0 {
962 if attempt > 2 {
964 panic!("This test should have completed by now (drain)");
965 }
966
967 cfg.interceptor_state()
968 .store_put(RequestAttempts::new(attempt));
969 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
970 assert!(matches!(should_retry, ShouldAttempt::YesAfterDelay(_)));
971 attempt += 1;
972 }
973
974 let permit = strategy.retry_permit.lock().unwrap().take().unwrap();
976 permit.forget();
977
978 ctx.set_output_or_error(Ok(Output::doesnt_matter()));
979
980 while token_bucket.available_permits() < PERMIT_COUNT {
982 if attempt > 23 {
983 panic!("This test should have completed by now (fill-up)");
984 }
985
986 cfg.interceptor_state()
987 .store_put(RequestAttempts::new(attempt));
988 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
989 assert_eq!(no_retry, ShouldAttempt::No);
990 attempt += 1;
991 }
992
993 assert_eq!(attempt, 23);
994 assert_eq!(token_bucket.available_permits(), PERMIT_COUNT);
995 }
996
997 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
998 #[test]
999 fn backoff_timing() {
1000 let (mut cfg, rc, ctx) = setup_test(
1001 vec![RetryAction::server_error()],
1002 RetryConfig::standard()
1003 .with_use_static_exponential_base(true)
1004 .with_max_attempts(5),
1005 );
1006 let strategy = StandardRetryStrategy::new();
1007 cfg.interceptor_state().store_put(TokenBucket::default());
1008 let token_bucket = cfg.load::<TokenBucket>().unwrap().clone();
1009
1010 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1011 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1012 let dur = should_retry.expect_delay();
1013 assert_eq!(dur, Duration::from_secs(1));
1014 assert_eq!(token_bucket.available_permits(), 495);
1015
1016 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1017 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1018 let dur = should_retry.expect_delay();
1019 assert_eq!(dur, Duration::from_secs(2));
1020 assert_eq!(token_bucket.available_permits(), 490);
1021
1022 cfg.interceptor_state().store_put(RequestAttempts::new(3));
1023 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1024 let dur = should_retry.expect_delay();
1025 assert_eq!(dur, Duration::from_secs(4));
1026 assert_eq!(token_bucket.available_permits(), 485);
1027
1028 cfg.interceptor_state().store_put(RequestAttempts::new(4));
1029 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1030 let dur = should_retry.expect_delay();
1031 assert_eq!(dur, Duration::from_secs(8));
1032 assert_eq!(token_bucket.available_permits(), 480);
1033
1034 cfg.interceptor_state().store_put(RequestAttempts::new(5));
1035 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1036 assert_eq!(no_retry, ShouldAttempt::No);
1037 assert_eq!(token_bucket.available_permits(), 480);
1038 }
1039
1040 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1041 #[test]
1042 fn max_backoff_time() {
1043 let (mut cfg, rc, ctx) = setup_test(
1044 vec![RetryAction::server_error()],
1045 RetryConfig::standard()
1046 .with_use_static_exponential_base(true)
1047 .with_max_attempts(5)
1048 .with_initial_backoff(Duration::from_secs(1))
1049 .with_max_backoff(Duration::from_secs(3)),
1050 );
1051 let strategy = StandardRetryStrategy::new();
1052 cfg.interceptor_state().store_put(TokenBucket::default());
1053 let token_bucket = cfg.load::<TokenBucket>().unwrap().clone();
1054
1055 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1056 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1057 let dur = should_retry.expect_delay();
1058 assert_eq!(dur, Duration::from_secs(1));
1059 assert_eq!(token_bucket.available_permits(), 495);
1060
1061 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1062 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1063 let dur = should_retry.expect_delay();
1064 assert_eq!(dur, Duration::from_secs(2));
1065 assert_eq!(token_bucket.available_permits(), 490);
1066
1067 cfg.interceptor_state().store_put(RequestAttempts::new(3));
1068 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1069 let dur = should_retry.expect_delay();
1070 assert_eq!(dur, Duration::from_secs(3));
1071 assert_eq!(token_bucket.available_permits(), 485);
1072
1073 cfg.interceptor_state().store_put(RequestAttempts::new(4));
1074 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1075 let dur = should_retry.expect_delay();
1076 assert_eq!(dur, Duration::from_secs(3));
1077 assert_eq!(token_bucket.available_permits(), 480);
1078
1079 cfg.interceptor_state().store_put(RequestAttempts::new(5));
1080 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1081 assert_eq!(no_retry, ShouldAttempt::No);
1082 assert_eq!(token_bucket.available_permits(), 480);
1083 }
1084
1085 const MAX_BACKOFF: Duration = Duration::from_secs(20);
1086
1087 #[test]
1088 fn calculate_exponential_backoff_where_initial_backoff_is_one() {
1089 let initial_backoff = 1.0;
1090
1091 for (attempt, expected_backoff) in [initial_backoff, 2.0, 4.0].into_iter().enumerate() {
1092 let actual_backoff =
1093 calculate_exponential_backoff(1.0, initial_backoff, attempt as u32, MAX_BACKOFF);
1094 assert_eq!(Duration::from_secs_f64(expected_backoff), actual_backoff);
1095 }
1096 }
1097
1098 #[test]
1099 fn calculate_exponential_backoff_where_initial_backoff_is_greater_than_one() {
1100 let initial_backoff = 3.0;
1101
1102 for (attempt, expected_backoff) in [initial_backoff, 6.0, 12.0].into_iter().enumerate() {
1103 let actual_backoff =
1104 calculate_exponential_backoff(1.0, initial_backoff, attempt as u32, MAX_BACKOFF);
1105 assert_eq!(Duration::from_secs_f64(expected_backoff), actual_backoff);
1106 }
1107 }
1108
1109 #[test]
1110 fn calculate_exponential_backoff_where_initial_backoff_is_less_than_one() {
1111 let initial_backoff = 0.03;
1112
1113 for (attempt, expected_backoff) in [initial_backoff, 0.06, 0.12].into_iter().enumerate() {
1114 let actual_backoff =
1115 calculate_exponential_backoff(1.0, initial_backoff, attempt as u32, MAX_BACKOFF);
1116 assert_eq!(Duration::from_secs_f64(expected_backoff), actual_backoff);
1117 }
1118 }
1119
1120 #[test]
1121 fn calculate_backoff_overflow_should_gracefully_fallback_to_max_backoff() {
1122 assert_eq!(
1124 MAX_BACKOFF,
1125 calculate_exponential_backoff(1_f64, 10_f64, 100000, MAX_BACKOFF),
1126 );
1127 }
1128
1129 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1130 #[test]
1131 fn v2_1_non_throttling_uses_50ms_backoff() {
1132 let (ctx, rc, cfg) = set_up_cfg_and_context(
1133 ErrorKind::ServerError,
1134 1,
1135 RetryConfig::standard()
1136 .with_use_static_exponential_base(true)
1137 .with_max_attempts(3)
1138 .with_retry_spec(RetrySpec::v2_1()),
1139 );
1140 let strategy = StandardRetryStrategy::new();
1141 let actual = strategy
1142 .should_attempt_retry(&ctx, &rc, &cfg)
1143 .expect("method is infallible for this use");
1144 assert_eq!(
1146 ShouldAttempt::YesAfterDelay(Duration::from_millis(50)),
1147 actual
1148 );
1149 }
1150
1151 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1152 #[test]
1153 fn v2_1_throttling_uses_1s_backoff() {
1154 let (ctx, rc, cfg) = set_up_cfg_and_context(
1155 ErrorKind::ThrottlingError,
1156 1,
1157 RetryConfig::standard()
1158 .with_use_static_exponential_base(true)
1159 .with_max_attempts(3)
1160 .with_retry_spec(RetrySpec::v2_1()),
1161 );
1162 let strategy = StandardRetryStrategy::new();
1163 let actual = strategy
1164 .should_attempt_retry(&ctx, &rc, &cfg)
1165 .expect("method is infallible for this use");
1166 assert_eq!(ShouldAttempt::YesAfterDelay(Duration::from_secs(1)), actual);
1168 }
1169
1170 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1171 #[test]
1172 fn v2_0_non_throttling_uses_1s_backoff() {
1173 let (ctx, rc, cfg) = set_up_cfg_and_context(
1174 ErrorKind::ServerError,
1175 1,
1176 RetryConfig::standard()
1177 .with_use_static_exponential_base(true)
1178 .with_max_attempts(3)
1179 .with_retry_spec(RetrySpec::v2_0()),
1180 );
1181 let strategy = StandardRetryStrategy::new();
1182 let actual = strategy
1183 .should_attempt_retry(&ctx, &rc, &cfg)
1184 .expect("method is infallible for this use");
1185 assert_eq!(ShouldAttempt::YesAfterDelay(Duration::from_secs(1)), actual);
1187 }
1188
1189 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1190 #[test]
1191 fn v2_1_retry_after_bounded_between_t_i_and_t_i_plus_5s() {
1192 let (mut cfg, rc, ctx) = setup_test(
1193 vec![RetryAction::retryable_error_with_explicit_delay(
1194 ErrorKind::ServerError,
1195 Duration::from_secs(3),
1196 )],
1197 RetryConfig::standard()
1198 .with_use_static_exponential_base(true)
1199 .with_max_attempts(3)
1200 .with_retry_spec(RetrySpec::v2_1()),
1201 );
1202 let strategy = StandardRetryStrategy::new();
1203 cfg.interceptor_state().store_put(TokenBucket::default());
1204 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1205 let actual = strategy
1206 .should_attempt_retry(&ctx, &rc, &cfg)
1207 .expect("method is infallible for this use");
1208 assert_eq!(ShouldAttempt::YesAfterDelay(Duration::from_secs(3)), actual);
1210 }
1211
1212 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1213 #[test]
1214 fn v2_1_retry_after_below_t_i_uses_t_i() {
1215 let (mut cfg, rc, ctx) = setup_test(
1216 vec![RetryAction::retryable_error_with_explicit_delay(
1217 ErrorKind::ServerError,
1218 Duration::from_millis(10),
1219 )],
1220 RetryConfig::standard()
1221 .with_use_static_exponential_base(true)
1222 .with_max_attempts(3)
1223 .with_retry_spec(RetrySpec::v2_1()),
1224 );
1225 let strategy = StandardRetryStrategy::new();
1226 cfg.interceptor_state().store_put(TokenBucket::default());
1227 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1228 let actual = strategy
1229 .should_attempt_retry(&ctx, &rc, &cfg)
1230 .expect("method is infallible for this use");
1231 assert_eq!(
1233 ShouldAttempt::YesAfterDelay(Duration::from_millis(50)),
1234 actual
1235 );
1236 }
1237
1238 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1239 #[test]
1240 fn v2_1_retry_after_above_t_i_plus_5s_capped() {
1241 let (mut cfg, rc, ctx) = setup_test(
1242 vec![RetryAction::retryable_error_with_explicit_delay(
1243 ErrorKind::ServerError,
1244 Duration::from_secs(10),
1245 )],
1246 RetryConfig::standard()
1247 .with_use_static_exponential_base(true)
1248 .with_max_attempts(3)
1249 .with_retry_spec(RetrySpec::v2_1()),
1250 );
1251 let strategy = StandardRetryStrategy::new();
1252 cfg.interceptor_state().store_put(TokenBucket::default());
1253 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1254 let actual = strategy
1255 .should_attempt_retry(&ctx, &rc, &cfg)
1256 .expect("method is infallible for this use");
1257 assert_eq!(
1259 ShouldAttempt::YesAfterDelay(Duration::from_millis(5050)),
1260 actual
1261 );
1262 }
1263
1264 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1265 #[test]
1266 fn v2_0_retry_after_capped_at_max_backoff() {
1267 let (mut cfg, rc, ctx) = setup_test(
1268 vec![RetryAction::retryable_error_with_explicit_delay(
1269 ErrorKind::ServerError,
1270 Duration::from_secs(30),
1271 )],
1272 RetryConfig::standard()
1273 .with_use_static_exponential_base(true)
1274 .with_max_attempts(3)
1275 .with_retry_spec(RetrySpec::v2_0()),
1276 );
1277 let strategy = StandardRetryStrategy::new();
1278 cfg.interceptor_state().store_put(TokenBucket::default());
1279 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1280 let actual = strategy
1281 .should_attempt_retry(&ctx, &rc, &cfg)
1282 .expect("method is infallible for this use");
1283 assert_eq!(
1285 ShouldAttempt::YesAfterDelay(Duration::from_secs(20)),
1286 actual
1287 );
1288 }
1289
1290 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1291 #[test]
1292 fn long_polling_backs_off_when_token_bucket_empty() {
1293 let (mut cfg, rc, ctx) = setup_test(
1294 vec![RetryAction::server_error()],
1295 RetryConfig::standard()
1296 .with_use_static_exponential_base(true)
1297 .with_max_attempts(5)
1298 .with_retry_spec(RetrySpec::v2_1().with_long_polling(true)),
1299 );
1300 let strategy = StandardRetryStrategy::new();
1301 cfg.interceptor_state().store_put(TokenBucket::new(0));
1302 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1303 let hint = LongPollingBackoff::default();
1304 cfg.interceptor_state().store_put(hint.clone());
1305
1306 let result = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1307 assert_eq!(result, ShouldAttempt::No);
1308 assert_eq!(hint.take(), Some(Duration::from_millis(50)));
1309 }
1310
1311 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1312 #[test]
1313 fn non_long_polling_no_backoff_when_token_bucket_empty() {
1314 let (mut cfg, rc, ctx) = setup_test(
1315 vec![RetryAction::server_error()],
1316 RetryConfig::standard()
1317 .with_use_static_exponential_base(true)
1318 .with_max_attempts(5)
1319 .with_retry_spec(RetrySpec::v2_0()),
1320 );
1321 let strategy = StandardRetryStrategy::new();
1322 cfg.interceptor_state().store_put(TokenBucket::new(0));
1323 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1324
1325 let result = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1326 assert_eq!(result, ShouldAttempt::No);
1327 }
1328
1329 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1330 fn v2_1_token_bucket_with_capacity(capacity: usize) -> TokenBucket {
1331 TokenBucket::builder()
1332 .capacity(capacity)
1333 .retry_cost(DEFAULT_RETRY_COST)
1334 .throttling_retry_cost(THROTTLING_RETRY_COST)
1335 .timeout_retry_cost(DEFAULT_RETRY_TIMEOUT_COST)
1336 .build()
1337 }
1338
1339 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1340 fn v2_1_token_bucket() -> TokenBucket {
1341 v2_1_token_bucket_with_capacity(DEFAULT_CAPACITY)
1342 }
1343
1344 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1345 #[test]
1346 fn retry_eventually_succeeds() {
1347 let (mut cfg, rc, mut ctx) = setup_test(
1348 vec![RetryAction::server_error()],
1349 RetryConfig::standard()
1350 .with_use_static_exponential_base(true)
1351 .with_retry_spec(RetrySpec::v2_1()),
1352 );
1353 let strategy = StandardRetryStrategy::new();
1354 let tb = v2_1_token_bucket();
1355 cfg.interceptor_state().store_put(tb.clone());
1356
1357 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1358 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1359 assert_eq!(should_retry.expect_delay(), Duration::from_millis(50));
1360 assert_eq!(
1361 tb.available_permits(),
1362 DEFAULT_CAPACITY - DEFAULT_RETRY_COST as usize
1363 );
1364
1365 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1366 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1367 assert_eq!(should_retry.expect_delay(), Duration::from_millis(100));
1368 assert_eq!(
1369 tb.available_permits(),
1370 DEFAULT_CAPACITY - 2 * DEFAULT_RETRY_COST as usize
1371 );
1372
1373 ctx.set_output_or_error(Ok(Output::doesnt_matter()));
1374 cfg.interceptor_state().store_put(RequestAttempts::new(3));
1375 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1376 assert_eq!(no_retry, ShouldAttempt::No);
1377 assert_eq!(
1378 tb.available_permits(),
1379 DEFAULT_CAPACITY - DEFAULT_RETRY_COST as usize
1380 );
1381 }
1382
1383 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1384 #[test]
1385 fn fail_due_to_max_attempts_reached() {
1386 let (mut cfg, rc, ctx) = setup_test(
1387 vec![RetryAction::server_error()],
1388 RetryConfig::standard()
1389 .with_use_static_exponential_base(true)
1390 .with_retry_spec(RetrySpec::v2_1()),
1391 );
1392 let strategy = StandardRetryStrategy::new();
1393 let tb = v2_1_token_bucket();
1394 cfg.interceptor_state().store_put(tb.clone());
1395
1396 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1397 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1398 assert_eq!(should_retry.expect_delay(), Duration::from_millis(50));
1399 assert_eq!(
1400 tb.available_permits(),
1401 DEFAULT_CAPACITY - DEFAULT_RETRY_COST as usize
1402 );
1403
1404 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1405 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1406 assert_eq!(should_retry.expect_delay(), Duration::from_millis(100));
1407 assert_eq!(
1408 tb.available_permits(),
1409 DEFAULT_CAPACITY - 2 * DEFAULT_RETRY_COST as usize
1410 );
1411
1412 cfg.interceptor_state().store_put(RequestAttempts::new(3));
1413 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1414 assert_eq!(no_retry, ShouldAttempt::No);
1415 assert_eq!(
1416 tb.available_permits(),
1417 DEFAULT_CAPACITY - 2 * DEFAULT_RETRY_COST as usize
1418 );
1419 }
1420
1421 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1422 #[test]
1423 fn retry_quota_reached_after_single_retry() {
1424 let (mut cfg, rc, ctx) = setup_test(
1425 vec![RetryAction::server_error()],
1426 RetryConfig::standard()
1427 .with_use_static_exponential_base(true)
1428 .with_max_attempts(5)
1429 .with_retry_spec(RetrySpec::v2_1()),
1430 );
1431 let strategy = StandardRetryStrategy::new();
1432 let tb = v2_1_token_bucket_with_capacity(DEFAULT_RETRY_COST as usize);
1433 cfg.interceptor_state().store_put(tb.clone());
1434
1435 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1436 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1437 assert_eq!(should_retry.expect_delay(), Duration::from_millis(50));
1438 assert_eq!(tb.available_permits(), 0);
1439
1440 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1441 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1442 assert_eq!(no_retry, ShouldAttempt::No);
1443 assert_eq!(tb.available_permits(), 0);
1444 }
1445
1446 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1447 #[test]
1448 fn no_retries_if_retry_quota_is_zero() {
1449 let (mut cfg, rc, ctx) = setup_test(
1450 vec![RetryAction::server_error()],
1451 RetryConfig::standard()
1452 .with_use_static_exponential_base(true)
1453 .with_retry_spec(RetrySpec::v2_1()),
1454 );
1455 let strategy = StandardRetryStrategy::new();
1456 let tb = v2_1_token_bucket_with_capacity(0);
1457 cfg.interceptor_state().store_put(tb.clone());
1458
1459 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1460 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1461 assert_eq!(no_retry, ShouldAttempt::No);
1462 assert_eq!(tb.available_permits(), 0);
1463 }
1464
1465 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1466 #[test]
1467 fn retry_stops_after_retry_quota_exhaustion() {
1468 let (mut cfg, rc, ctx) = setup_test(
1469 vec![RetryAction::server_error()],
1470 RetryConfig::standard()
1471 .with_use_static_exponential_base(true)
1472 .with_max_attempts(5)
1473 .with_retry_spec(RetrySpec::v2_1()),
1474 );
1475 let strategy = StandardRetryStrategy::new();
1476 let tb = v2_1_token_bucket_with_capacity(20);
1477 cfg.interceptor_state().store_put(tb.clone());
1478
1479 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1480 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1481 assert_eq!(should_retry.expect_delay(), Duration::from_millis(50));
1482 assert_eq!(tb.available_permits(), 20 - DEFAULT_RETRY_COST as usize);
1483
1484 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1485 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1486 assert_eq!(no_retry, ShouldAttempt::No);
1487 assert_eq!(tb.available_permits(), 20 - DEFAULT_RETRY_COST as usize);
1488 }
1489
1490 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1491 #[test]
1492 fn retry_quota_recovery_after_successful_responses() {
1493 let (mut cfg, rc, mut ctx) = setup_test(
1494 vec![RetryAction::server_error()],
1495 RetryConfig::standard()
1496 .with_use_static_exponential_base(true)
1497 .with_max_attempts(5)
1498 .with_retry_spec(RetrySpec::v2_1()),
1499 );
1500 let strategy = StandardRetryStrategy::new();
1501 let tb = v2_1_token_bucket_with_capacity(30);
1502 cfg.interceptor_state().store_put(tb.clone());
1503
1504 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1505 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1506 assert_eq!(should_retry.expect_delay(), Duration::from_millis(50));
1507 assert_eq!(tb.available_permits(), 30 - DEFAULT_RETRY_COST as usize);
1508
1509 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1510 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1511 assert_eq!(should_retry.expect_delay(), Duration::from_millis(100));
1512 assert_eq!(tb.available_permits(), 30 - 2 * DEFAULT_RETRY_COST as usize);
1513
1514 ctx.set_output_or_error(Ok(Output::doesnt_matter()));
1515 cfg.interceptor_state().store_put(RequestAttempts::new(3));
1516 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1517 assert_eq!(no_retry, ShouldAttempt::No);
1518 assert_eq!(tb.available_permits(), 30 - DEFAULT_RETRY_COST as usize);
1519
1520 ctx.set_output_or_error(Err(OrchestratorError::other("doesn't matter")));
1521 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1522 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1523 assert_eq!(should_retry.expect_delay(), Duration::from_millis(50));
1524 assert_eq!(tb.available_permits(), 30 - 2 * DEFAULT_RETRY_COST as usize);
1525
1526 ctx.set_output_or_error(Ok(Output::doesnt_matter()));
1527 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1528 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1529 assert_eq!(no_retry, ShouldAttempt::No);
1530 assert_eq!(tb.available_permits(), 30 - DEFAULT_RETRY_COST as usize);
1531 }
1532
1533 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1534 #[test]
1535 fn throttling_error_token_bucket_drain_and_backoff() {
1536 let (mut cfg, rc, mut ctx) = setup_test(
1537 vec![RetryAction::retryable_error(ErrorKind::ThrottlingError)],
1538 RetryConfig::standard()
1539 .with_use_static_exponential_base(true)
1540 .with_retry_spec(RetrySpec::v2_1()),
1541 );
1542 let strategy = StandardRetryStrategy::new();
1543 let tb = v2_1_token_bucket();
1544 cfg.interceptor_state().store_put(tb.clone());
1545
1546 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1547 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1548 assert_eq!(should_retry.expect_delay(), Duration::from_secs(1));
1549 assert_eq!(
1550 tb.available_permits(),
1551 DEFAULT_CAPACITY - THROTTLING_RETRY_COST as usize
1552 );
1553
1554 ctx.set_output_or_error(Ok(Output::doesnt_matter()));
1555 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1556 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1557 assert_eq!(no_retry, ShouldAttempt::No);
1558 assert_eq!(tb.available_permits(), DEFAULT_CAPACITY);
1559 }
1560
1561 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1562 #[test]
1563 fn long_polling_backoff_after_throttling_error_when_token_bucket_empty() {
1564 let (mut cfg, rc, ctx) = setup_test(
1565 vec![RetryAction::retryable_error(ErrorKind::ThrottlingError)],
1566 RetryConfig::standard()
1567 .with_use_static_exponential_base(true)
1568 .with_max_attempts(5)
1569 .with_retry_spec(RetrySpec::v2_1().with_long_polling(true)),
1570 );
1571 let strategy = StandardRetryStrategy::new();
1572 let tb = v2_1_token_bucket_with_capacity(0);
1573 cfg.interceptor_state().store_put(tb.clone());
1574 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1575 let hint = LongPollingBackoff::default();
1576 cfg.interceptor_state().store_put(hint.clone());
1577
1578 let result = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1579 assert_eq!(result, ShouldAttempt::No);
1580 assert_eq!(hint.take(), Some(Duration::from_secs(1)));
1581 assert_eq!(tb.available_permits(), 0);
1582 }
1583
1584 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1585 #[test]
1586 fn long_polling_max_attempts_exceeded_must_not_delay() {
1587 let (mut cfg, rc, ctx) = setup_test(
1588 vec![RetryAction::server_error()],
1589 RetryConfig::standard()
1590 .with_use_static_exponential_base(true)
1591 .with_max_attempts(2)
1592 .with_retry_spec(RetrySpec::v2_1().with_long_polling(true)),
1593 );
1594 let strategy = StandardRetryStrategy::new();
1595 let tb = v2_1_token_bucket();
1596 cfg.interceptor_state().store_put(tb.clone());
1597
1598 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1599 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1600 assert_eq!(should_retry.expect_delay(), Duration::from_millis(50));
1601
1602 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1603 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1604 assert_eq!(no_retry, ShouldAttempt::No);
1605 }
1606
1607 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1608 #[test]
1609 fn long_polling_success_must_not_delay() {
1610 let (mut cfg, rc, mut ctx) = setup_test(
1611 vec![RetryAction::server_error()],
1612 RetryConfig::standard()
1613 .with_use_static_exponential_base(true)
1614 .with_max_attempts(2)
1615 .with_retry_spec(RetrySpec::v2_1().with_long_polling(true)),
1616 );
1617 let strategy = StandardRetryStrategy::new();
1618 let tb = v2_1_token_bucket();
1619 cfg.interceptor_state().store_put(tb.clone());
1620
1621 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1622 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1623 assert_eq!(should_retry.expect_delay(), Duration::from_millis(50));
1624
1625 ctx.set_output_or_error(Ok(Output::doesnt_matter()));
1626 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1627 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1628 assert_eq!(no_retry, ShouldAttempt::No);
1629 }
1630
1631 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1632 #[test]
1633 fn long_polling_non_retryable_errors_must_not_delay() {
1634 let (mut cfg, rc, ctx) = setup_test(
1635 vec![RetryAction::NoActionIndicated],
1636 RetryConfig::standard()
1637 .with_use_static_exponential_base(true)
1638 .with_max_attempts(2)
1639 .with_retry_spec(RetrySpec::v2_1().with_long_polling(true)),
1640 );
1641 let strategy = StandardRetryStrategy::new();
1642 let tb = v2_1_token_bucket();
1643 cfg.interceptor_state().store_put(tb.clone());
1644 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1645
1646 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1647 assert_eq!(no_retry, ShouldAttempt::No);
1648 }
1649
1650 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
1651 #[test]
1652 fn verify_max_backoff_time() {
1653 let (mut cfg, rc, ctx) = setup_test(
1654 vec![RetryAction::server_error()],
1655 RetryConfig::standard()
1656 .with_use_static_exponential_base(true)
1657 .with_max_attempts(5)
1658 .with_max_backoff(Duration::from_millis(200))
1659 .with_retry_spec(RetrySpec::v2_1()),
1660 );
1661 let strategy = StandardRetryStrategy::new();
1662 let tb = v2_1_token_bucket();
1663 cfg.interceptor_state().store_put(tb.clone());
1664
1665 cfg.interceptor_state().store_put(RequestAttempts::new(1));
1666 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1667 assert_eq!(should_retry.expect_delay(), Duration::from_millis(50));
1668 assert_eq!(
1669 tb.available_permits(),
1670 DEFAULT_CAPACITY - DEFAULT_RETRY_COST as usize
1671 );
1672
1673 cfg.interceptor_state().store_put(RequestAttempts::new(2));
1674 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1675 assert_eq!(should_retry.expect_delay(), Duration::from_millis(100));
1676 assert_eq!(
1677 tb.available_permits(),
1678 DEFAULT_CAPACITY - 2 * DEFAULT_RETRY_COST as usize
1679 );
1680
1681 cfg.interceptor_state().store_put(RequestAttempts::new(3));
1682 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1683 assert_eq!(should_retry.expect_delay(), Duration::from_millis(200));
1684 assert_eq!(
1685 tb.available_permits(),
1686 DEFAULT_CAPACITY - 3 * DEFAULT_RETRY_COST as usize
1687 );
1688
1689 cfg.interceptor_state().store_put(RequestAttempts::new(4));
1691 let should_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1692 assert_eq!(should_retry.expect_delay(), Duration::from_millis(200));
1693 assert_eq!(
1694 tb.available_permits(),
1695 DEFAULT_CAPACITY - 4 * DEFAULT_RETRY_COST as usize
1696 );
1697
1698 cfg.interceptor_state().store_put(RequestAttempts::new(5));
1699 let no_retry = strategy.should_attempt_retry(&ctx, &rc, &cfg).unwrap();
1700 assert_eq!(no_retry, ShouldAttempt::No);
1701 assert_eq!(
1702 tb.available_permits(),
1703 DEFAULT_CAPACITY - 4 * DEFAULT_RETRY_COST as usize
1704 );
1705 }
1706}