aws_smithy_runtime_api/client/retries/
classifiers.rs1use crate::box_error::BoxError;
31use crate::client::interceptors::context::InterceptorContext;
32use crate::client::runtime_components::sealed::ValidateConfig;
33use crate::client::runtime_components::RuntimeComponents;
34use crate::impl_shared_conversions;
35use aws_smithy_types::config_bag::ConfigBag;
36use aws_smithy_types::retry::ErrorKind;
37use std::fmt;
38use std::sync::Arc;
39use std::time::Duration;
40
41#[non_exhaustive]
43#[derive(Clone, Eq, PartialEq, Debug, Default)]
44pub enum RetryAction {
45 #[default]
51 NoActionIndicated,
52 RetryIndicated(RetryReason),
54 RetryForbidden,
58}
59
60impl fmt::Display for RetryAction {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 Self::NoActionIndicated => write!(f, "no action indicated"),
64 Self::RetryForbidden => write!(f, "retry forbidden"),
65 Self::RetryIndicated(reason) => write!(f, "retry {reason}"),
66 }
67 }
68}
69
70impl RetryAction {
71 pub fn retryable_error(kind: ErrorKind) -> Self {
73 Self::RetryIndicated(RetryReason::RetryableError {
74 kind,
75 retry_after: None,
76 })
77 }
78
79 pub fn retryable_error_with_explicit_delay(kind: ErrorKind, retry_after: Duration) -> Self {
81 Self::RetryIndicated(RetryReason::RetryableError {
82 kind,
83 retry_after: Some(retry_after),
84 })
85 }
86
87 pub fn transient_error() -> Self {
89 Self::retryable_error(ErrorKind::TransientError)
90 }
91
92 pub fn throttling_error() -> Self {
94 Self::retryable_error(ErrorKind::ThrottlingError)
95 }
96
97 pub fn server_error() -> Self {
99 Self::retryable_error(ErrorKind::ServerError)
100 }
101
102 pub fn client_error() -> Self {
104 Self::retryable_error(ErrorKind::ClientError)
105 }
106
107 pub fn should_retry(&self) -> bool {
109 match self {
110 Self::NoActionIndicated | Self::RetryForbidden => false,
111 Self::RetryIndicated(_) => true,
112 }
113 }
114}
115
116#[non_exhaustive]
118#[derive(Clone, Eq, PartialEq, Debug)]
119pub enum RetryReason {
120 RetryableError {
122 kind: ErrorKind,
124 retry_after: Option<Duration>,
126 },
127}
128
129impl fmt::Display for RetryReason {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 match self {
132 Self::RetryableError { kind, retry_after } => {
133 let after = retry_after
134 .map(|d| format!(" after {d:?}"))
135 .unwrap_or_default();
136 write!(f, "{kind} error{after}")
137 }
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct RetryClassifierPriority {
148 inner: Inner,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152enum Inner {
153 HttpStatusCodeClassifier,
155 ModeledAsRetryableClassifier,
157 TransientErrorClassifier,
159 Other(i8),
161}
162
163impl PartialOrd for RetryClassifierPriority {
164 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
165 Some(self.cmp(other))
166 }
167}
168
169impl Ord for RetryClassifierPriority {
170 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
171 self.as_i8().cmp(&other.as_i8())
172 }
173}
174
175impl RetryClassifierPriority {
176 pub fn http_status_code_classifier() -> Self {
178 Self {
179 inner: Inner::HttpStatusCodeClassifier,
180 }
181 }
182
183 pub fn modeled_as_retryable_classifier() -> Self {
185 Self {
186 inner: Inner::ModeledAsRetryableClassifier,
187 }
188 }
189
190 pub fn transient_error_classifier() -> Self {
192 Self {
193 inner: Inner::TransientErrorClassifier,
194 }
195 }
196
197 #[deprecated = "use the less-confusingly-named `RetryClassifierPriority::run_before` instead"]
198 pub fn with_lower_priority_than(other: Self) -> Self {
200 Self::run_before(other)
201 }
202
203 pub fn run_before(other: Self) -> Self {
208 Self {
209 inner: Inner::Other(other.as_i8() - 1),
210 }
211 }
212
213 #[deprecated = "use the less-confusingly-named `RetryClassifierPriority::run_after` instead"]
214 pub fn with_higher_priority_than(other: Self) -> Self {
216 Self::run_after(other)
217 }
218
219 pub fn run_after(other: Self) -> Self {
224 Self {
225 inner: Inner::Other(other.as_i8() + 1),
226 }
227 }
228
229 fn as_i8(&self) -> i8 {
230 match self.inner {
231 Inner::HttpStatusCodeClassifier => 0,
232 Inner::ModeledAsRetryableClassifier => 10,
233 Inner::TransientErrorClassifier => 20,
234 Inner::Other(i) => i,
235 }
236 }
237}
238
239impl Default for RetryClassifierPriority {
240 fn default() -> Self {
241 Self {
242 inner: Inner::Other(0),
243 }
244 }
245}
246
247pub trait ClassifyRetry: Send + Sync + fmt::Debug {
249 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction;
257
258 fn classify_retry_v2(&self, ctx: &InterceptorContext, previous: &RetryAction) -> RetryAction {
269 let _ = previous;
270 self.classify_retry(ctx)
271 }
272
273 fn name(&self) -> &'static str;
277
278 fn priority(&self) -> RetryClassifierPriority {
290 RetryClassifierPriority::default()
291 }
292}
293
294impl_shared_conversions!(convert SharedRetryClassifier from ClassifyRetry using SharedRetryClassifier::new);
295
296#[derive(Debug, Clone)]
297pub struct SharedRetryClassifier(Arc<dyn ClassifyRetry>);
299
300impl SharedRetryClassifier {
301 pub fn new(retry_classifier: impl ClassifyRetry + 'static) -> Self {
303 Self(Arc::new(retry_classifier))
304 }
305}
306
307impl ClassifyRetry for SharedRetryClassifier {
308 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
309 self.0.classify_retry(ctx)
310 }
311
312 fn classify_retry_v2(&self, ctx: &InterceptorContext, previous: &RetryAction) -> RetryAction {
313 self.0.classify_retry_v2(ctx, previous)
314 }
315
316 fn name(&self) -> &'static str {
317 self.0.name()
318 }
319
320 fn priority(&self) -> RetryClassifierPriority {
321 self.0.priority()
322 }
323}
324
325impl ValidateConfig for SharedRetryClassifier {
326 fn validate_final_config(
327 &self,
328 _runtime_components: &RuntimeComponents,
329 _cfg: &ConfigBag,
330 ) -> Result<(), BoxError> {
331 #[cfg(debug_assertions)]
332 {
333 let retry_classifiers = _runtime_components.retry_classifiers_slice();
336 let out_of_order: Vec<_> = retry_classifiers
337 .windows(2)
338 .filter(|&w| w[0].value().priority() > w[1].value().priority())
339 .collect();
340
341 if !out_of_order.is_empty() {
342 return Err("retry classifiers are mis-ordered; this is a bug".into());
343 }
344 }
345 Ok(())
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::{ClassifyRetry, RetryAction, RetryClassifierPriority, SharedRetryClassifier};
352 use crate::client::interceptors::context::{Input, InterceptorContext};
353
354 #[test]
355 fn test_preset_priorities() {
356 let before_modeled_as_retryable = RetryClassifierPriority::run_before(
357 RetryClassifierPriority::modeled_as_retryable_classifier(),
358 );
359 let mut list = vec![
360 RetryClassifierPriority::modeled_as_retryable_classifier(),
361 RetryClassifierPriority::http_status_code_classifier(),
362 RetryClassifierPriority::transient_error_classifier(),
363 before_modeled_as_retryable,
364 ];
365 list.sort();
366
367 assert_eq!(
368 vec![
369 RetryClassifierPriority::http_status_code_classifier(),
370 before_modeled_as_retryable,
371 RetryClassifierPriority::modeled_as_retryable_classifier(),
372 RetryClassifierPriority::transient_error_classifier(),
373 ],
374 list
375 );
376 }
377
378 #[test]
379 fn test_classifier_run_before() {
380 let high_priority_classifier = RetryClassifierPriority::default();
382 let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
383 let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
384
385 let mut list = vec![
386 mid_priority_classifier,
387 high_priority_classifier,
388 low_priority_classifier,
389 ];
390 list.sort();
391
392 assert_eq!(
393 vec![
394 low_priority_classifier,
395 mid_priority_classifier,
396 high_priority_classifier
397 ],
398 list
399 );
400 }
401
402 #[test]
403 fn test_classifier_run_after() {
404 let low_priority_classifier = RetryClassifierPriority::default();
406 let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
407 let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
408
409 let mut list = vec![
410 mid_priority_classifier,
411 low_priority_classifier,
412 high_priority_classifier,
413 ];
414 list.sort();
415
416 assert_eq!(
417 vec![
418 low_priority_classifier,
419 mid_priority_classifier,
420 high_priority_classifier
421 ],
422 list
423 );
424 }
425
426 #[derive(Debug)]
427 struct ClassifierStub {
428 name: &'static str,
429 priority: RetryClassifierPriority,
430 }
431
432 impl ClassifyRetry for ClassifierStub {
433 fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
434 todo!()
435 }
436
437 fn name(&self) -> &'static str {
438 self.name
439 }
440
441 fn priority(&self) -> RetryClassifierPriority {
442 self.priority
443 }
444 }
445
446 fn wrap(name: &'static str, priority: RetryClassifierPriority) -> SharedRetryClassifier {
447 SharedRetryClassifier::new(ClassifierStub { name, priority })
448 }
449
450 #[test]
451 fn test_shared_classifier_run_before() {
452 let high_priority_classifier = RetryClassifierPriority::default();
455 let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
456 let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
457
458 let mut list = [
459 wrap("mid", mid_priority_classifier),
460 wrap("high", high_priority_classifier),
461 wrap("low", low_priority_classifier),
462 ];
463 list.sort_by_key(|rc| rc.priority());
464
465 let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
466 assert_eq!(vec!["low", "mid", "high"], actual);
467 }
468
469 #[test]
470 fn test_shared_classifier_run_after() {
471 let low_priority_classifier = RetryClassifierPriority::default();
474 let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
475 let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
476
477 let mut list = [
478 wrap("mid", mid_priority_classifier),
479 wrap("high", high_priority_classifier),
480 wrap("low", low_priority_classifier),
481 ];
482 list.sort_by_key(|rc| rc.priority());
483
484 let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
485 assert_eq!(vec!["low", "mid", "high"], actual);
486 }
487
488 #[test]
489 fn test_shared_preset_priorities() {
490 let before_modeled_as_retryable = RetryClassifierPriority::run_before(
491 RetryClassifierPriority::modeled_as_retryable_classifier(),
492 );
493 let mut list = [
494 wrap(
495 "modeled as retryable",
496 RetryClassifierPriority::modeled_as_retryable_classifier(),
497 ),
498 wrap(
499 "http status code",
500 RetryClassifierPriority::http_status_code_classifier(),
501 ),
502 wrap(
503 "transient error",
504 RetryClassifierPriority::transient_error_classifier(),
505 ),
506 wrap("before 'modeled as retryable'", before_modeled_as_retryable),
507 ];
508 list.sort_by_key(|rc| rc.priority());
509
510 let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
511 assert_eq!(
512 vec![
513 "http status code",
514 "before 'modeled as retryable'",
515 "modeled as retryable",
516 "transient error"
517 ],
518 actual
519 );
520 }
521
522 fn test_ctx() -> InterceptorContext {
523 InterceptorContext::new(Input::erase("input"))
524 }
525
526 #[derive(Debug)]
529 struct FixedClassifier(RetryAction);
530
531 impl ClassifyRetry for FixedClassifier {
532 fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
533 self.0.clone()
534 }
535
536 fn name(&self) -> &'static str {
537 "fixed"
538 }
539 }
540
541 #[test]
545 fn default_classify_retry_v2_delegates_and_ignores_previous() {
546 let ctx = test_ctx();
547 let classifier = FixedClassifier(RetryAction::transient_error());
548
549 for previous in [
551 RetryAction::NoActionIndicated,
552 RetryAction::RetryForbidden,
553 RetryAction::throttling_error(),
554 ] {
555 assert_eq!(
556 classifier.classify_retry_v2(&ctx, &previous),
557 classifier.classify_retry(&ctx),
558 );
559 }
560 }
561
562 #[derive(Debug)]
565 struct PreviousEchoClassifier;
566
567 impl ClassifyRetry for PreviousEchoClassifier {
568 fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
569 RetryAction::NoActionIndicated
570 }
571
572 fn classify_retry_v2(
573 &self,
574 _ctx: &InterceptorContext,
575 previous: &RetryAction,
576 ) -> RetryAction {
577 previous.clone()
578 }
579
580 fn name(&self) -> &'static str {
581 "previous-echo"
582 }
583 }
584
585 #[test]
588 fn shared_classifier_forwards_classify_retry_v2_to_inner() {
589 let ctx = test_ctx();
590 let shared = SharedRetryClassifier::new(PreviousEchoClassifier);
591
592 assert_eq!(
595 shared.classify_retry_v2(&ctx, &RetryAction::throttling_error()),
596 RetryAction::throttling_error(),
597 );
598
599 assert_eq!(shared.classify_retry(&ctx), RetryAction::NoActionIndicated);
601 }
602}