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.as_i8().cmp(&other.as_i8()))
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;
252
253 fn name(&self) -> &'static str;
257
258 fn priority(&self) -> RetryClassifierPriority {
270 RetryClassifierPriority::default()
271 }
272}
273
274impl_shared_conversions!(convert SharedRetryClassifier from ClassifyRetry using SharedRetryClassifier::new);
275
276#[derive(Debug, Clone)]
277pub struct SharedRetryClassifier(Arc<dyn ClassifyRetry>);
279
280impl SharedRetryClassifier {
281 pub fn new(retry_classifier: impl ClassifyRetry + 'static) -> Self {
283 Self(Arc::new(retry_classifier))
284 }
285}
286
287impl ClassifyRetry for SharedRetryClassifier {
288 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
289 self.0.classify_retry(ctx)
290 }
291
292 fn name(&self) -> &'static str {
293 self.0.name()
294 }
295
296 fn priority(&self) -> RetryClassifierPriority {
297 self.0.priority()
298 }
299}
300
301impl ValidateConfig for SharedRetryClassifier {
302 fn validate_final_config(
303 &self,
304 _runtime_components: &RuntimeComponents,
305 _cfg: &ConfigBag,
306 ) -> Result<(), BoxError> {
307 #[cfg(debug_assertions)]
308 {
309 let retry_classifiers = _runtime_components.retry_classifiers_slice();
312 let out_of_order: Vec<_> = retry_classifiers
313 .windows(2)
314 .filter(|&w| w[0].value().priority() > w[1].value().priority())
315 .collect();
316
317 if !out_of_order.is_empty() {
318 return Err("retry classifiers are mis-ordered; this is a bug".into());
319 }
320 }
321 Ok(())
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::{ClassifyRetry, RetryAction, RetryClassifierPriority, SharedRetryClassifier};
328 use crate::client::interceptors::context::InterceptorContext;
329
330 #[test]
331 fn test_preset_priorities() {
332 let before_modeled_as_retryable = RetryClassifierPriority::run_before(
333 RetryClassifierPriority::modeled_as_retryable_classifier(),
334 );
335 let mut list = vec![
336 RetryClassifierPriority::modeled_as_retryable_classifier(),
337 RetryClassifierPriority::http_status_code_classifier(),
338 RetryClassifierPriority::transient_error_classifier(),
339 before_modeled_as_retryable,
340 ];
341 list.sort();
342
343 assert_eq!(
344 vec![
345 RetryClassifierPriority::http_status_code_classifier(),
346 before_modeled_as_retryable,
347 RetryClassifierPriority::modeled_as_retryable_classifier(),
348 RetryClassifierPriority::transient_error_classifier(),
349 ],
350 list
351 );
352 }
353
354 #[test]
355 fn test_classifier_run_before() {
356 let high_priority_classifier = RetryClassifierPriority::default();
358 let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
359 let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
360
361 let mut list = vec![
362 mid_priority_classifier,
363 high_priority_classifier,
364 low_priority_classifier,
365 ];
366 list.sort();
367
368 assert_eq!(
369 vec![
370 low_priority_classifier,
371 mid_priority_classifier,
372 high_priority_classifier
373 ],
374 list
375 );
376 }
377
378 #[test]
379 fn test_classifier_run_after() {
380 let low_priority_classifier = RetryClassifierPriority::default();
382 let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
383 let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
384
385 let mut list = vec![
386 mid_priority_classifier,
387 low_priority_classifier,
388 high_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 #[derive(Debug)]
403 struct ClassifierStub {
404 name: &'static str,
405 priority: RetryClassifierPriority,
406 }
407
408 impl ClassifyRetry for ClassifierStub {
409 fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
410 todo!()
411 }
412
413 fn name(&self) -> &'static str {
414 self.name
415 }
416
417 fn priority(&self) -> RetryClassifierPriority {
418 self.priority
419 }
420 }
421
422 fn wrap(name: &'static str, priority: RetryClassifierPriority) -> SharedRetryClassifier {
423 SharedRetryClassifier::new(ClassifierStub { name, priority })
424 }
425
426 #[test]
427 fn test_shared_classifier_run_before() {
428 let high_priority_classifier = RetryClassifierPriority::default();
431 let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
432 let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
433
434 let mut list = vec![
435 wrap("mid", mid_priority_classifier),
436 wrap("high", high_priority_classifier),
437 wrap("low", low_priority_classifier),
438 ];
439 list.sort_by_key(|rc| rc.priority());
440
441 let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
442 assert_eq!(vec!["low", "mid", "high"], actual);
443 }
444
445 #[test]
446 fn test_shared_classifier_run_after() {
447 let low_priority_classifier = RetryClassifierPriority::default();
450 let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
451 let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
452
453 let mut list = vec![
454 wrap("mid", mid_priority_classifier),
455 wrap("high", high_priority_classifier),
456 wrap("low", low_priority_classifier),
457 ];
458 list.sort_by_key(|rc| rc.priority());
459
460 let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
461 assert_eq!(vec!["low", "mid", "high"], actual);
462 }
463
464 #[test]
465 fn test_shared_preset_priorities() {
466 let before_modeled_as_retryable = RetryClassifierPriority::run_before(
467 RetryClassifierPriority::modeled_as_retryable_classifier(),
468 );
469 let mut list = vec![
470 wrap(
471 "modeled as retryable",
472 RetryClassifierPriority::modeled_as_retryable_classifier(),
473 ),
474 wrap(
475 "http status code",
476 RetryClassifierPriority::http_status_code_classifier(),
477 ),
478 wrap(
479 "transient error",
480 RetryClassifierPriority::transient_error_classifier(),
481 ),
482 wrap("before 'modeled as retryable'", before_modeled_as_retryable),
483 ];
484 list.sort_by_key(|rc| rc.priority());
485
486 let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
487 assert_eq!(
488 vec![
489 "http status code",
490 "before 'modeled as retryable'",
491 "modeled as retryable",
492 "transient error"
493 ],
494 actual
495 );
496 }
497}