-
Notifications
You must be signed in to change notification settings - Fork 281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
retry: Add StandardRetryPolicy
and standard_policy
mod
#698
base: master
Are you sure you want to change the base?
Conversation
This PR adds a new `standard_policy` module within the retry module that provides a batteries included policy to be used with the retry middleware. The policy combines the `Budget` type and generic backoff utlities from the `backoff` module to provide an easy to use policy with good defaults. This PR also includes a `StandardRetryPolicyBuilder` as well as two new traits `IsRetryable` and `CloneRequest`. These each have blanket impls for closures. The reason that this implementation breaks these out into two different traits is to allow `tower-http` to provide a custom `CloneRequest` implementation that will be able to clone some sort of `ReplayBody` and let the user pass in the retry decision implementation. Ref #682
716f9d0
to
88b7df9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
|
||
pub mod backoff; | ||
pub mod budget; | ||
pub mod future; | ||
mod layer; | ||
mod policy; | ||
pub mod standard_policy; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how I feel about calling this "standard policy"; I might prefer a name that actually describes what the policy does, rather than just saying it's "standard"...
tower/src/retry/standard_policy.rs
Outdated
/// A trait to determine if the request associated with the response should be | ||
/// retried by [`StandardRetryPolicy`]. | ||
/// | ||
/// # Closure | ||
/// | ||
/// This trait provides a blanket implementation for a closure of the type | ||
/// `Fn(&mut Result<Res, E>) -> bool + Send + Sync + 'static`. | ||
pub trait IsRetryable<Res, E>: Send + Sync + 'static { | ||
/// Return `true` if the request associated with the response should be | ||
/// retried and `false` if it should not be retried. | ||
fn is_retryalbe(&self, response: &mut Result<Res, E>) -> bool; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, it seems like this trait should be able to consider the request as well as the response in determining whether a result is retryable. For example, some HTTP request methods are idempotent, and others are non-idempotent; and non-idempotent requests should generally not be retried. With the current implementation, the IsRetryable
function can't determine that a given request method is never retryable (without some kind of unfortunate contortion to stuff metadata about the request into the response type, or something).
It seems like it would be good to allow the request type to be considered as part of making this decision, especially if we want the StandardPolicy
to be usable for HTTP retries...
/// A trait to clone a request for the [`StandardRetryPolicy`]. | ||
/// | ||
/// # Closure | ||
/// | ||
/// This trait provides a blanket implementation for a closure of the type | ||
/// `Fn(&Req) -> Option<Req> + Send + Sync + 'static`. | ||
pub trait CloneRequest<Req>: Send + Sync + 'static { | ||
/// Clone a request, if `None` is returned the request will not be retried. | ||
fn clone_request(&self, request: &Req) -> Option<Req>; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems like there probably ought to be an implementation of this that's impl CloneRequest<Req> where Req: Clone
that just calls Clone
. Perhaps that ought to be the default?
//! - [`IsRetryable`] by default will always return `false`. | ||
//! - [`CloneRequest`] by default will always return `None`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems kind of unfortunate to me that the default behavior for the batteries-included retry policy is to...never retry anything. In particular, defaulting to a CloneRequest
implementation that always returns None
feels a bit weird to me.
What do you think about an API where the default implementation expects that the request type implements Clone
, and just calls its Clone
method? That way, users whose requests implement Clone
never have to think about the CloneRequest
trait, and providing a CloneRequest
implementation is only necessary if you want to retry !Clone
requests?
//! | ||
//! The [`budget`] module contains utilities to reduce the amount of concurrent | ||
//! retries made by a tower middleware stack. The goal for this is to reduce | ||
//! congestive collapse when downstream systems degrade. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could maybe be nice to make "congestive collapse" a link to Wikipedia or something?
tower/src/retry/mod.rs
Outdated
//! The [`standard_policy`] module contains a default retry [`Policy`] that can | ||
//! be used with the [`Retry`] middleware. For more information check the module | ||
//! docs for [`standard_policy`]. | ||
//! | ||
//! The [`backoff`] module contains backoff utlities that can be used in a | ||
//! custom [`Policy`]. | ||
//! | ||
//! The [`budget`] module contains utilities to reduce the amount of concurrent | ||
//! retries made by a tower middleware stack. The goal for this is to reduce | ||
//! congestive collapse when downstream systems degrade. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
take it or leave it: it seems kind of like these paragraphs could make sense as a bulleted list?
} | ||
} | ||
|
||
/// A standard retry [`Policy`] that combines a retry budget and a backoff |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe "retry budget" and "backoff" here should link to the respective modules?
is_retryable: Arc<dyn IsRetryable<Res, E>>, | ||
clone_request: Arc<dyn CloneRequest<Req>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a little bit on the fence about whether always Arc
ing these is the right choice or not --- a question that's definitely come up in the past with other middleware that needs to clone a Fn
value. for example, MapRequest
is generic over a F: FnMut(...) + Clone
, and MapResponse
is generic over a FnOnce + Clone
, rather than Arc
ing a Fn
...
tower/tower/src/builder/mod.rs
Line 200 in 878b10f
F: FnMut(R1) -> R2 + Clone, |
in this case, since the functions are always cloned into each request, it wouldn't make sense to allow FnMut + Clone
, since the mutable state wouldn't be shared across instances of the closure. but, it would allow the use of function pointers (not closures) to avoid allocating an Arc
and bumping two reference counts on every request...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, maybe a microoptimization, but it could be worth considering combining the IsRetryable
and CloneRequest
impls into a single struct that's then Arc
ed, so that we only increment/decrement one atomic ref count on each request, rather than two...but, OTTOH, we would probably have to either box them or make the struct generic, if we did that, and boxing them would mean two heap ptr derefs...:woman_shrugging:
Co-authored-by: Eliza Weisman <[email protected]>
This PR adds a new
standard_policy
module within the retry module that provides a batteries included policy to be used with the retry middleware. The policy combines theBudget
type and generic backoff utlities from thebackoff
module to provide an easy to use policy with good defaults.This PR also includes a
StandardRetryPolicyBuilder
as well as two new traitsIsRetryable
andCloneRequest
. These each have blanket impls for closures. The reason that this implementation breaks these out into two different traits is to allowtower-http
to provide a customCloneRequest
implementation that will be able to clone some sort ofReplayBody
and let the user pass in the retry decision implementation.Ref #682