use crate::box_error::BoxError;
use crate::client::interceptors::context::InterceptorContext;
use crate::client::runtime_components::sealed::ValidateConfig;
use crate::client::runtime_components::RuntimeComponents;
use crate::impl_shared_conversions;
use aws_smithy_types::config_bag::ConfigBag;
use aws_smithy_types::retry::ErrorKind;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
#[non_exhaustive]
#[derive(Clone, Eq, PartialEq, Debug, Default)]
pub enum RetryAction {
#[default]
NoActionIndicated,
RetryIndicated(RetryReason),
RetryForbidden,
}
impl fmt::Display for RetryAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoActionIndicated => write!(f, "no action indicated"),
Self::RetryForbidden => write!(f, "retry forbidden"),
Self::RetryIndicated(reason) => write!(f, "retry {reason}"),
}
}
}
impl RetryAction {
pub fn retryable_error(kind: ErrorKind) -> Self {
Self::RetryIndicated(RetryReason::RetryableError {
kind,
retry_after: None,
})
}
pub fn retryable_error_with_explicit_delay(kind: ErrorKind, retry_after: Duration) -> Self {
Self::RetryIndicated(RetryReason::RetryableError {
kind,
retry_after: Some(retry_after),
})
}
pub fn transient_error() -> Self {
Self::retryable_error(ErrorKind::TransientError)
}
pub fn throttling_error() -> Self {
Self::retryable_error(ErrorKind::ThrottlingError)
}
pub fn server_error() -> Self {
Self::retryable_error(ErrorKind::ServerError)
}
pub fn client_error() -> Self {
Self::retryable_error(ErrorKind::ClientError)
}
pub fn should_retry(&self) -> bool {
match self {
Self::NoActionIndicated | Self::RetryForbidden => false,
Self::RetryIndicated(_) => true,
}
}
}
#[non_exhaustive]
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum RetryReason {
RetryableError {
kind: ErrorKind,
retry_after: Option<Duration>,
},
}
impl fmt::Display for RetryReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RetryableError { kind, retry_after } => {
let after = retry_after
.map(|d| format!(" after {d:?}"))
.unwrap_or_default();
write!(f, "{kind} error{after}")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryClassifierPriority {
inner: Inner,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Inner {
HttpStatusCodeClassifier,
ModeledAsRetryableClassifier,
TransientErrorClassifier,
Other(i8),
}
impl PartialOrd for RetryClassifierPriority {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RetryClassifierPriority {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_i8().cmp(&other.as_i8())
}
}
impl RetryClassifierPriority {
pub fn http_status_code_classifier() -> Self {
Self {
inner: Inner::HttpStatusCodeClassifier,
}
}
pub fn modeled_as_retryable_classifier() -> Self {
Self {
inner: Inner::ModeledAsRetryableClassifier,
}
}
pub fn transient_error_classifier() -> Self {
Self {
inner: Inner::TransientErrorClassifier,
}
}
#[deprecated = "use the less-confusingly-named `RetryClassifierPriority::run_before` instead"]
pub fn with_lower_priority_than(other: Self) -> Self {
Self::run_before(other)
}
pub fn run_before(other: Self) -> Self {
Self {
inner: Inner::Other(other.as_i8() - 1),
}
}
#[deprecated = "use the less-confusingly-named `RetryClassifierPriority::run_after` instead"]
pub fn with_higher_priority_than(other: Self) -> Self {
Self::run_after(other)
}
pub fn run_after(other: Self) -> Self {
Self {
inner: Inner::Other(other.as_i8() + 1),
}
}
fn as_i8(&self) -> i8 {
match self.inner {
Inner::HttpStatusCodeClassifier => 0,
Inner::ModeledAsRetryableClassifier => 10,
Inner::TransientErrorClassifier => 20,
Inner::Other(i) => i,
}
}
}
impl Default for RetryClassifierPriority {
fn default() -> Self {
Self {
inner: Inner::Other(0),
}
}
}
pub trait ClassifyRetry: Send + Sync + fmt::Debug {
fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction;
fn name(&self) -> &'static str;
fn priority(&self) -> RetryClassifierPriority {
RetryClassifierPriority::default()
}
}
impl_shared_conversions!(convert SharedRetryClassifier from ClassifyRetry using SharedRetryClassifier::new);
#[derive(Debug, Clone)]
pub struct SharedRetryClassifier(Arc<dyn ClassifyRetry>);
impl SharedRetryClassifier {
pub fn new(retry_classifier: impl ClassifyRetry + 'static) -> Self {
Self(Arc::new(retry_classifier))
}
}
impl ClassifyRetry for SharedRetryClassifier {
fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
self.0.classify_retry(ctx)
}
fn name(&self) -> &'static str {
self.0.name()
}
fn priority(&self) -> RetryClassifierPriority {
self.0.priority()
}
}
impl ValidateConfig for SharedRetryClassifier {
fn validate_final_config(
&self,
_runtime_components: &RuntimeComponents,
_cfg: &ConfigBag,
) -> Result<(), BoxError> {
#[cfg(debug_assertions)]
{
let retry_classifiers = _runtime_components.retry_classifiers_slice();
let out_of_order: Vec<_> = retry_classifiers
.windows(2)
.filter(|&w| w[0].value().priority() > w[1].value().priority())
.collect();
if !out_of_order.is_empty() {
return Err("retry classifiers are mis-ordered; this is a bug".into());
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{ClassifyRetry, RetryAction, RetryClassifierPriority, SharedRetryClassifier};
use crate::client::interceptors::context::InterceptorContext;
#[test]
fn test_preset_priorities() {
let before_modeled_as_retryable = RetryClassifierPriority::run_before(
RetryClassifierPriority::modeled_as_retryable_classifier(),
);
let mut list = vec![
RetryClassifierPriority::modeled_as_retryable_classifier(),
RetryClassifierPriority::http_status_code_classifier(),
RetryClassifierPriority::transient_error_classifier(),
before_modeled_as_retryable,
];
list.sort();
assert_eq!(
vec![
RetryClassifierPriority::http_status_code_classifier(),
before_modeled_as_retryable,
RetryClassifierPriority::modeled_as_retryable_classifier(),
RetryClassifierPriority::transient_error_classifier(),
],
list
);
}
#[test]
fn test_classifier_run_before() {
let high_priority_classifier = RetryClassifierPriority::default();
let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
let mut list = vec![
mid_priority_classifier,
high_priority_classifier,
low_priority_classifier,
];
list.sort();
assert_eq!(
vec![
low_priority_classifier,
mid_priority_classifier,
high_priority_classifier
],
list
);
}
#[test]
fn test_classifier_run_after() {
let low_priority_classifier = RetryClassifierPriority::default();
let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
let mut list = vec![
mid_priority_classifier,
low_priority_classifier,
high_priority_classifier,
];
list.sort();
assert_eq!(
vec![
low_priority_classifier,
mid_priority_classifier,
high_priority_classifier
],
list
);
}
#[derive(Debug)]
struct ClassifierStub {
name: &'static str,
priority: RetryClassifierPriority,
}
impl ClassifyRetry for ClassifierStub {
fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
todo!()
}
fn name(&self) -> &'static str {
self.name
}
fn priority(&self) -> RetryClassifierPriority {
self.priority
}
}
fn wrap(name: &'static str, priority: RetryClassifierPriority) -> SharedRetryClassifier {
SharedRetryClassifier::new(ClassifierStub { name, priority })
}
#[test]
fn test_shared_classifier_run_before() {
let high_priority_classifier = RetryClassifierPriority::default();
let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
let mut list = [
wrap("mid", mid_priority_classifier),
wrap("high", high_priority_classifier),
wrap("low", low_priority_classifier),
];
list.sort_by_key(|rc| rc.priority());
let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
assert_eq!(vec!["low", "mid", "high"], actual);
}
#[test]
fn test_shared_classifier_run_after() {
let low_priority_classifier = RetryClassifierPriority::default();
let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
let mut list = [
wrap("mid", mid_priority_classifier),
wrap("high", high_priority_classifier),
wrap("low", low_priority_classifier),
];
list.sort_by_key(|rc| rc.priority());
let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
assert_eq!(vec!["low", "mid", "high"], actual);
}
#[test]
fn test_shared_preset_priorities() {
let before_modeled_as_retryable = RetryClassifierPriority::run_before(
RetryClassifierPriority::modeled_as_retryable_classifier(),
);
let mut list = [
wrap(
"modeled as retryable",
RetryClassifierPriority::modeled_as_retryable_classifier(),
),
wrap(
"http status code",
RetryClassifierPriority::http_status_code_classifier(),
),
wrap(
"transient error",
RetryClassifierPriority::transient_error_classifier(),
),
wrap("before 'modeled as retryable'", before_modeled_as_retryable),
];
list.sort_by_key(|rc| rc.priority());
let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
assert_eq!(
vec![
"http status code",
"before 'modeled as retryable'",
"modeled as retryable",
"transient error"
],
actual
);
}
}