aws_smithy_runtime/client/retries/
classifiers.rs1use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
7use aws_smithy_runtime_api::client::retries::classifiers::{
8 ClassifyRetry, RetryAction, RetryClassifierPriority, SharedRetryClassifier,
9};
10use aws_smithy_types::retry::ProvideErrorKind;
11use std::borrow::Cow;
12use std::error::Error as StdError;
13use std::marker::PhantomData;
14
15#[derive(Debug, Default)]
17pub struct ModeledAsRetryableClassifier<E> {
18 _inner: PhantomData<E>,
19}
20
21impl<E> ModeledAsRetryableClassifier<E> {
22 pub fn new() -> Self {
24 Self {
25 _inner: PhantomData,
26 }
27 }
28
29 pub fn priority() -> RetryClassifierPriority {
31 RetryClassifierPriority::modeled_as_retryable_classifier()
32 }
33}
34
35impl<E> ClassifyRetry for ModeledAsRetryableClassifier<E>
36where
37 E: StdError + ProvideErrorKind + Send + Sync + 'static,
38{
39 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
40 let output_or_error = ctx.output_or_error();
42 let error = match output_or_error {
44 Some(Ok(_)) | None => return RetryAction::NoActionIndicated,
45 Some(Err(err)) => err,
46 };
47 error
49 .as_operation_error()
50 .and_then(|err| err.downcast_ref::<E>())
52 .and_then(|err| err.retryable_error_kind().map(RetryAction::retryable_error))
54 .unwrap_or_default()
55 }
56
57 fn name(&self) -> &'static str {
58 "Errors Modeled As Retryable"
59 }
60
61 fn priority(&self) -> RetryClassifierPriority {
62 Self::priority()
63 }
64}
65
66#[derive(Debug, Default)]
68pub struct TransientErrorClassifier<E> {
69 _inner: PhantomData<E>,
70}
71
72impl<E> TransientErrorClassifier<E> {
73 pub fn new() -> Self {
75 Self {
76 _inner: PhantomData,
77 }
78 }
79
80 pub fn priority() -> RetryClassifierPriority {
82 RetryClassifierPriority::transient_error_classifier()
83 }
84}
85
86impl<E> ClassifyRetry for TransientErrorClassifier<E>
87where
88 E: StdError + Send + Sync + 'static,
89{
90 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
91 let output_or_error = ctx.output_or_error();
93 let error = match output_or_error {
95 Some(Ok(_)) | None => return RetryAction::NoActionIndicated,
96 Some(Err(err)) => err,
97 };
98
99 if error.is_response_error() || error.is_timeout_error() {
100 RetryAction::transient_error()
101 } else if let Some(error) = error.as_connector_error() {
102 if error.is_timeout() || error.is_io() {
103 RetryAction::transient_error()
104 } else {
105 error
106 .as_other()
107 .map(RetryAction::retryable_error)
108 .unwrap_or_default()
109 }
110 } else {
111 RetryAction::NoActionIndicated
112 }
113 }
114
115 fn name(&self) -> &'static str {
116 "Retryable Smithy Errors"
117 }
118
119 fn priority(&self) -> RetryClassifierPriority {
120 Self::priority()
121 }
122}
123
124const TRANSIENT_ERROR_STATUS_CODES: &[u16] = &[500, 502, 503, 504];
125
126#[derive(Debug)]
129pub struct HttpStatusCodeClassifier {
130 retryable_status_codes: Cow<'static, [u16]>,
131}
132
133impl Default for HttpStatusCodeClassifier {
134 fn default() -> Self {
135 Self::new_from_codes(TRANSIENT_ERROR_STATUS_CODES.to_owned())
136 }
137}
138
139impl HttpStatusCodeClassifier {
140 pub fn new_from_codes(retryable_status_codes: impl Into<Cow<'static, [u16]>>) -> Self {
144 Self {
145 retryable_status_codes: retryable_status_codes.into(),
146 }
147 }
148
149 pub fn priority() -> RetryClassifierPriority {
151 RetryClassifierPriority::http_status_code_classifier()
152 }
153}
154
155impl ClassifyRetry for HttpStatusCodeClassifier {
156 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
157 let is_retryable = ctx
158 .response()
159 .map(|res| res.status().into())
160 .map(|status| self.retryable_status_codes.contains(&status))
161 .unwrap_or_default();
162
163 if is_retryable {
164 RetryAction::transient_error()
165 } else {
166 RetryAction::NoActionIndicated
167 }
168 }
169
170 fn name(&self) -> &'static str {
171 "HTTP Status Code"
172 }
173
174 fn priority(&self) -> RetryClassifierPriority {
175 Self::priority()
176 }
177}
178
179pub fn run_classifiers_on_ctx(
184 classifiers: impl Iterator<Item = SharedRetryClassifier>,
185 ctx: &InterceptorContext,
186) -> RetryAction {
187 let mut result = RetryAction::NoActionIndicated;
189
190 for classifier in classifiers {
191 let new_result = classifier.classify_retry_v2(ctx, &result);
192
193 if new_result == RetryAction::NoActionIndicated {
196 continue;
197 }
198
199 tracing::trace!(
201 "Classifier '{}' set the result of classification to '{}'",
202 classifier.name(),
203 new_result
204 );
205 result = new_result;
206
207 if result == RetryAction::RetryForbidden {
209 tracing::trace!("retry classification ending early because a `RetryAction::RetryForbidden` was emitted",);
210 break;
211 }
212 }
213
214 result
215}
216
217#[cfg(test)]
218mod test {
219 use crate::client::retries::classifiers::{
220 HttpStatusCodeClassifier, ModeledAsRetryableClassifier,
221 };
222 use aws_smithy_runtime_api::client::interceptors::context::{Error, Input, InterceptorContext};
223 use aws_smithy_runtime_api::client::orchestrator::OrchestratorError;
224 use aws_smithy_runtime_api::client::retries::classifiers::{
225 ClassifyRetry, RetryAction, RetryReason, SharedRetryClassifier,
226 };
227 use aws_smithy_types::body::SdkBody;
228 use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind};
229 use std::fmt;
230 use std::time::Duration;
231
232 use super::{run_classifiers_on_ctx, TransientErrorClassifier};
233
234 #[derive(Debug, PartialEq, Eq, Clone)]
235 struct UnmodeledError;
236
237 impl fmt::Display for UnmodeledError {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 write!(f, "UnmodeledError")
240 }
241 }
242
243 impl std::error::Error for UnmodeledError {}
244
245 #[test]
246 fn classify_by_response_status() {
247 let policy = HttpStatusCodeClassifier::default();
248 let res = http_1x::Response::builder()
249 .status(500)
250 .body("error!")
251 .unwrap()
252 .map(SdkBody::from);
253 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
254 ctx.set_response(res.try_into().unwrap());
255 assert_eq!(policy.classify_retry(&ctx), RetryAction::transient_error());
256 }
257
258 #[test]
259 fn classify_by_response_status_not_retryable() {
260 let policy = HttpStatusCodeClassifier::default();
261 let res = http_1x::Response::builder()
262 .status(408)
263 .body("error!")
264 .unwrap()
265 .map(SdkBody::from);
266 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
267 ctx.set_response(res.try_into().unwrap());
268 assert_eq!(policy.classify_retry(&ctx), RetryAction::NoActionIndicated);
269 }
270
271 #[test]
272 fn classify_by_error_kind() {
273 #[derive(Debug)]
274 struct RetryableError;
275
276 impl fmt::Display for RetryableError {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 write!(f, "Some retryable error")
279 }
280 }
281
282 impl ProvideErrorKind for RetryableError {
283 fn retryable_error_kind(&self) -> Option<ErrorKind> {
284 Some(ErrorKind::ClientError)
285 }
286
287 fn code(&self) -> Option<&str> {
288 unimplemented!()
290 }
291 }
292
293 impl std::error::Error for RetryableError {}
294
295 let policy = ModeledAsRetryableClassifier::<RetryableError>::new();
296 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
297 ctx.set_output_or_error(Err(OrchestratorError::operation(Error::erase(
298 RetryableError,
299 ))));
300
301 assert_eq!(policy.classify_retry(&ctx), RetryAction::client_error(),);
302 }
303
304 #[test]
305 fn classify_response_error() {
306 let policy = TransientErrorClassifier::<UnmodeledError>::new();
307 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
308 ctx.set_output_or_error(Err(OrchestratorError::response(
309 "I am a response error".into(),
310 )));
311 assert_eq!(policy.classify_retry(&ctx), RetryAction::transient_error(),);
312 }
313
314 #[test]
315 fn test_timeout_error() {
316 let policy = TransientErrorClassifier::<UnmodeledError>::new();
317 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
318 ctx.set_output_or_error(Err(OrchestratorError::timeout(
319 "I am a timeout error".into(),
320 )));
321 assert_eq!(policy.classify_retry(&ctx), RetryAction::transient_error(),);
322 }
323
324 #[derive(Debug)]
327 struct StaticClassifier {
328 name: &'static str,
329 action: RetryAction,
330 }
331
332 impl ClassifyRetry for StaticClassifier {
333 fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
334 self.action.clone()
335 }
336
337 fn name(&self) -> &'static str {
338 self.name
339 }
340 }
341
342 #[derive(Debug)]
348 struct DelayRefiningClassifier;
349
350 impl ClassifyRetry for DelayRefiningClassifier {
351 fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
352 RetryAction::NoActionIndicated
353 }
354
355 fn classify_retry_v2(
356 &self,
357 _ctx: &InterceptorContext,
358 previous: &RetryAction,
359 ) -> RetryAction {
360 if let RetryAction::RetryIndicated(RetryReason::RetryableError {
361 kind,
362 retry_after: None,
363 }) = previous
364 {
365 RetryAction::retryable_error_with_explicit_delay(*kind, Duration::from_secs(5))
366 } else {
367 RetryAction::NoActionIndicated
368 }
369 }
370
371 fn name(&self) -> &'static str {
372 "delay-refining"
373 }
374 }
375
376 #[test]
380 fn run_classifiers_on_ctx_lets_later_classifier_refine_earlier_verdict() {
381 let ctx = InterceptorContext::new(Input::doesnt_matter());
382 let classifiers = vec![
383 SharedRetryClassifier::new(StaticClassifier {
384 name: "transient",
385 action: RetryAction::transient_error(),
386 }),
387 SharedRetryClassifier::new(DelayRefiningClassifier),
388 ];
389
390 assert_eq!(
391 run_classifiers_on_ctx(classifiers.into_iter(), &ctx),
392 RetryAction::retryable_error_with_explicit_delay(
393 ErrorKind::TransientError,
394 Duration::from_secs(5),
395 ),
396 );
397 }
398
399 #[test]
405 fn run_classifiers_on_ctx_refiner_does_not_fabricate_retry() {
406 let ctx = InterceptorContext::new(Input::doesnt_matter());
407 let classifiers = vec![SharedRetryClassifier::new(DelayRefiningClassifier)];
408
409 assert_eq!(
410 run_classifiers_on_ctx(classifiers.into_iter(), &ctx),
411 RetryAction::NoActionIndicated,
412 );
413 }
414
415 #[test]
419 fn run_classifiers_on_ctx_preserves_earlier_verdict_when_refiner_abstains() {
420 let ctx = InterceptorContext::new(Input::doesnt_matter());
421 let already_delayed = RetryAction::retryable_error_with_explicit_delay(
422 ErrorKind::TransientError,
423 Duration::from_secs(1),
424 );
425 let classifiers = vec![
426 SharedRetryClassifier::new(StaticClassifier {
427 name: "already-delayed",
428 action: already_delayed.clone(),
429 }),
430 SharedRetryClassifier::new(DelayRefiningClassifier),
431 ];
432
433 assert_eq!(
434 run_classifiers_on_ctx(classifiers.into_iter(), &ctx),
435 already_delayed,
436 );
437 }
438}