1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
56//! Classifiers for determining if a retry is necessary and related code.
7//!
8//! When a request fails, a retry strategy should inspect the result with retry
9//! classifiers to understand if and how the request should be retried.
10//!
11//! Because multiple classifiers are often used, and because some are more
12//! specific than others in what they identify as retryable, classifiers are
13//! run in a sequence that is determined by their priority.
14//!
15//! Classifiers that are higher priority are run **after** classifiers
16//! with a lower priority. The intention is that:
17//!
18//! 1. Generic classifiers that look at things like the HTTP error code run
19//! first.
20//! 2. More specific classifiers such as ones that check for certain error
21//! messages are run **after** the generic classifiers. This gives them the
22//! ability to override the actions set by the generic retry classifiers.
23//!
24//! Put another way:
25//!
26//! | large nets target common failures with basic behavior | run before | small nets target specific failures with special behavior|
27//! |-------------------------------------------------------|-----------------------|----------------------------------------------------------|
28//! | low priority classifiers | results overridden by | high priority classifiers |
2930use 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;
4041/// The result of running a [`ClassifyRetry`] on a [`InterceptorContext`].
42#[non_exhaustive]
43#[derive(Clone, Eq, PartialEq, Debug, Default)]
44pub enum RetryAction {
45/// When a classifier can't run or has no opinion, this action is returned.
46 ///
47 /// For example, if a classifier requires a parsed response and response parsing failed,
48 /// this action is returned. If all classifiers return this action, no retry should be
49 /// attempted.
50#[default]
51NoActionIndicated,
52/// When a classifier runs and thinks a response should be retried, this action is returned.
53RetryIndicated(RetryReason),
54/// When a classifier runs and decides a response must not be retried, this action is returned.
55 ///
56 /// This action stops retry classification immediately, skipping any following classifiers.
57RetryForbidden,
58}
5960impl fmt::Display for RetryAction {
61fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62match self {
63Self::NoActionIndicated => write!(f, "no action indicated"),
64Self::RetryForbidden => write!(f, "retry forbidden"),
65Self::RetryIndicated(reason) => write!(f, "retry {reason}"),
66 }
67 }
68}
6970impl RetryAction {
71/// Create a new `RetryAction` indicating that a retry is necessary.
72pub fn retryable_error(kind: ErrorKind) -> Self {
73Self::RetryIndicated(RetryReason::RetryableError {
74 kind,
75 retry_after: None,
76 })
77 }
7879/// Create a new `RetryAction` indicating that a retry is necessary after an explicit delay.
80pub fn retryable_error_with_explicit_delay(kind: ErrorKind, retry_after: Duration) -> Self {
81Self::RetryIndicated(RetryReason::RetryableError {
82 kind,
83 retry_after: Some(retry_after),
84 })
85 }
8687/// Create a new `RetryAction` indicating that a retry is necessary because of a transient error.
88pub fn transient_error() -> Self {
89Self::retryable_error(ErrorKind::TransientError)
90 }
9192/// Create a new `RetryAction` indicating that a retry is necessary because of a throttling error.
93pub fn throttling_error() -> Self {
94Self::retryable_error(ErrorKind::ThrottlingError)
95 }
9697/// Create a new `RetryAction` indicating that a retry is necessary because of a server error.
98pub fn server_error() -> Self {
99Self::retryable_error(ErrorKind::ServerError)
100 }
101102/// Create a new `RetryAction` indicating that a retry is necessary because of a client error.
103pub fn client_error() -> Self {
104Self::retryable_error(ErrorKind::ClientError)
105 }
106107/// Check if a retry is indicated.
108pub fn should_retry(&self) -> bool {
109match self {
110Self::NoActionIndicated | Self::RetryForbidden => false,
111Self::RetryIndicated(_) => true,
112 }
113 }
114}
115116/// The reason for a retry.
117#[non_exhaustive]
118#[derive(Clone, Eq, PartialEq, Debug)]
119pub enum RetryReason {
120/// When an error is received that should be retried, this reason is returned.
121RetryableError {
122/// The kind of error.
123kind: ErrorKind,
124/// A server may tell us to retry only after a specific time has elapsed.
125retry_after: Option<Duration>,
126 },
127}
128129impl fmt::Display for RetryReason {
130fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131match self {
132Self::RetryableError { kind, retry_after } => {
133let after = retry_after
134 .map(|d| format!(" after {d:?}"))
135 .unwrap_or_default();
136write!(f, "{kind} error{after}")
137 }
138 }
139 }
140}
141142/// The priority of a retry classifier. Classifiers with a higher priority will
143/// run **after** classifiers with a lower priority and may override their
144/// result. Classifiers with equal priorities make no guarantees about which
145/// will run first.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct RetryClassifierPriority {
148 inner: Inner,
149}
150151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152enum Inner {
153/// The default priority for the `HttpStatusCodeClassifier`.
154HttpStatusCodeClassifier,
155/// The default priority for the `ModeledAsRetryableClassifier`.
156ModeledAsRetryableClassifier,
157/// The default priority for the `TransientErrorClassifier`.
158TransientErrorClassifier,
159/// The priority of some other classifier.
160Other(i8),
161}
162163impl PartialOrd for RetryClassifierPriority {
164fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
165Some(self.as_i8().cmp(&other.as_i8()))
166 }
167}
168169impl Ord for RetryClassifierPriority {
170fn cmp(&self, other: &Self) -> std::cmp::Ordering {
171self.as_i8().cmp(&other.as_i8())
172 }
173}
174175impl RetryClassifierPriority {
176/// Create a new `RetryClassifierPriority` with the default priority for the `HttpStatusCodeClassifier`.
177pub fn http_status_code_classifier() -> Self {
178Self {
179 inner: Inner::HttpStatusCodeClassifier,
180 }
181 }
182183/// Create a new `RetryClassifierPriority` with the default priority for the `ModeledAsRetryableClassifier`.
184pub fn modeled_as_retryable_classifier() -> Self {
185Self {
186 inner: Inner::ModeledAsRetryableClassifier,
187 }
188 }
189190/// Create a new `RetryClassifierPriority` with the default priority for the `TransientErrorClassifier`.
191pub fn transient_error_classifier() -> Self {
192Self {
193 inner: Inner::TransientErrorClassifier,
194 }
195 }
196197#[deprecated = "use the less-confusingly-named `RetryClassifierPriority::run_before` instead"]
198/// Create a new `RetryClassifierPriority` with lower priority than the given priority.
199pub fn with_lower_priority_than(other: Self) -> Self {
200Self::run_before(other)
201 }
202203/// Create a new `RetryClassifierPriority` that can be overridden by the given priority.
204 ///
205 /// Retry classifiers are run in order from lowest to highest priority. A classifier that
206 /// runs later can override a decision from a classifier that runs earlier.
207pub fn run_before(other: Self) -> Self {
208Self {
209 inner: Inner::Other(other.as_i8() - 1),
210 }
211 }
212213#[deprecated = "use the less-confusingly-named `RetryClassifierPriority::run_after` instead"]
214/// Create a new `RetryClassifierPriority` with higher priority than the given priority.
215pub fn with_higher_priority_than(other: Self) -> Self {
216Self::run_after(other)
217 }
218219/// Create a new `RetryClassifierPriority` that can override the given priority.
220 ///
221 /// Retry classifiers are run in order from lowest to highest priority. A classifier that
222 /// runs later can override a decision from a classifier that runs earlier.
223pub fn run_after(other: Self) -> Self {
224Self {
225 inner: Inner::Other(other.as_i8() + 1),
226 }
227 }
228229fn as_i8(&self) -> i8 {
230match self.inner {
231 Inner::HttpStatusCodeClassifier => 0,
232 Inner::ModeledAsRetryableClassifier => 10,
233 Inner::TransientErrorClassifier => 20,
234 Inner::Other(i) => i,
235 }
236 }
237}
238239impl Default for RetryClassifierPriority {
240fn default() -> Self {
241Self {
242 inner: Inner::Other(0),
243 }
244 }
245}
246247/// Classifies what kind of retry is needed for a given [`InterceptorContext`].
248pub trait ClassifyRetry: Send + Sync + fmt::Debug {
249/// Run this classifier on the [`InterceptorContext`] to determine if the previous request
250 /// should be retried. Returns a [`RetryAction`].
251fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction;
252253/// The name of this retry classifier.
254 ///
255 /// Used for debugging purposes.
256fn name(&self) -> &'static str;
257258/// The priority of this retry classifier.
259 ///
260 /// Classifiers with a higher priority will override the
261 /// results of classifiers with a lower priority. Classifiers with equal priorities make no
262 /// guarantees about which will override the other.
263 ///
264 /// Retry classifiers are run in order of increasing priority. Any decision
265 /// (return value other than `NoActionIndicated`) from a higher priority
266 /// classifier will override the decision of a lower priority classifier with one exception:
267 /// [`RetryAction::RetryForbidden`] is treated differently: If ANY classifier returns `RetryForbidden`,
268 /// this request will not be retried.
269fn priority(&self) -> RetryClassifierPriority {
270 RetryClassifierPriority::default()
271 }
272}
273274impl_shared_conversions!(convert SharedRetryClassifier from ClassifyRetry using SharedRetryClassifier::new);
275276#[derive(Debug, Clone)]
277/// Retry classifier used by the retry strategy to classify responses as retryable or not.
278pub struct SharedRetryClassifier(Arc<dyn ClassifyRetry>);
279280impl SharedRetryClassifier {
281/// Given a [`ClassifyRetry`] trait object, create a new `SharedRetryClassifier`.
282pub fn new(retry_classifier: impl ClassifyRetry + 'static) -> Self {
283Self(Arc::new(retry_classifier))
284 }
285}
286287impl ClassifyRetry for SharedRetryClassifier {
288fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
289self.0.classify_retry(ctx)
290 }
291292fn name(&self) -> &'static str {
293self.0.name()
294 }
295296fn priority(&self) -> RetryClassifierPriority {
297self.0.priority()
298 }
299}
300301impl ValidateConfig for SharedRetryClassifier {
302fn validate_final_config(
303&self,
304 _runtime_components: &RuntimeComponents,
305 _cfg: &ConfigBag,
306 ) -> Result<(), BoxError> {
307#[cfg(debug_assertions)]
308{
309// Because this is validating that the implementation is correct rather
310 // than validating user input, we only want to run this in debug builds.
311let retry_classifiers = _runtime_components.retry_classifiers_slice();
312let out_of_order: Vec<_> = retry_classifiers
313 .windows(2)
314 .filter(|&w| w[0].value().priority() > w[1].value().priority())
315 .collect();
316317if !out_of_order.is_empty() {
318return Err("retry classifiers are mis-ordered; this is a bug".into());
319 }
320 }
321Ok(())
322 }
323}
324325#[cfg(test)]
326mod tests {
327use super::{ClassifyRetry, RetryAction, RetryClassifierPriority, SharedRetryClassifier};
328use crate::client::interceptors::context::InterceptorContext;
329330#[test]
331fn test_preset_priorities() {
332let before_modeled_as_retryable = RetryClassifierPriority::run_before(
333 RetryClassifierPriority::modeled_as_retryable_classifier(),
334 );
335let 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();
342343assert_eq!(
344vec![
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 }
353354#[test]
355fn test_classifier_run_before() {
356// Ensure low-priority classifiers run *before* high-priority classifiers.
357let high_priority_classifier = RetryClassifierPriority::default();
358let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
359let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
360361let mut list = vec![
362 mid_priority_classifier,
363 high_priority_classifier,
364 low_priority_classifier,
365 ];
366 list.sort();
367368assert_eq!(
369vec![
370 low_priority_classifier,
371 mid_priority_classifier,
372 high_priority_classifier
373 ],
374 list
375 );
376 }
377378#[test]
379fn test_classifier_run_after() {
380// Ensure high-priority classifiers run *after* low-priority classifiers.
381let low_priority_classifier = RetryClassifierPriority::default();
382let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
383let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
384385let mut list = vec![
386 mid_priority_classifier,
387 low_priority_classifier,
388 high_priority_classifier,
389 ];
390 list.sort();
391392assert_eq!(
393vec![
394 low_priority_classifier,
395 mid_priority_classifier,
396 high_priority_classifier
397 ],
398 list
399 );
400 }
401402#[derive(Debug)]
403struct ClassifierStub {
404 name: &'static str,
405 priority: RetryClassifierPriority,
406 }
407408impl ClassifyRetry for ClassifierStub {
409fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
410todo!()
411 }
412413fn name(&self) -> &'static str {
414self.name
415 }
416417fn priority(&self) -> RetryClassifierPriority {
418self.priority
419 }
420 }
421422fn wrap(name: &'static str, priority: RetryClassifierPriority) -> SharedRetryClassifier {
423 SharedRetryClassifier::new(ClassifierStub { name, priority })
424 }
425426#[test]
427fn test_shared_classifier_run_before() {
428// Ensure low-priority classifiers run *before* high-priority classifiers,
429 // even after wrapping.
430let high_priority_classifier = RetryClassifierPriority::default();
431let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
432let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
433434let 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());
440441let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
442assert_eq!(vec!["low", "mid", "high"], actual);
443 }
444445#[test]
446fn test_shared_classifier_run_after() {
447// Ensure high-priority classifiers run *after* low-priority classifiers,
448 // even after wrapping.
449let low_priority_classifier = RetryClassifierPriority::default();
450let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
451let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
452453let 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());
459460let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
461assert_eq!(vec!["low", "mid", "high"], actual);
462 }
463464#[test]
465fn test_shared_preset_priorities() {
466let before_modeled_as_retryable = RetryClassifierPriority::run_before(
467 RetryClassifierPriority::modeled_as_retryable_classifier(),
468 );
469let 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());
485486let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
487assert_eq!(
488vec![
489"http status code",
490"before 'modeled as retryable'",
491"modeled as retryable",
492"transient error"
493],
494 actual
495 );
496 }
497}