hpx/retry.rs
1//! Retry requests
2//!
3//! A `Client` has the ability to retry requests, by sending additional copies
4//! to the server if a response is considered retryable.
5//!
6//! The [`Policy`] makes it easier to configure what requests to retry, along
7//! with including best practices by default, such as a retry budget.
8//!
9//! # Defaults
10//!
11//! The default retry behavior of a `Client` is to only retry requests where an
12//! error or low-level protocol NACK is encountered that is known to be safe to
13//! retry. Note however that providing a specific retry policy will override
14//! the default, and you will need to explicitly include that behavior.
15//!
16//! All policies default to including a retry budget that permits 20% extra
17//! requests to be sent.
18//!
19//! # Scoped
20//!
21//! A client's retry policy is scoped. That means that the policy doesn't
22//! apply to all requests, but only those within a user-defined scope.
23//!
24//! Since all policies include a budget by default, it doesn't make sense to
25//! apply it on _all_ requests. Rather, the retry history applied by a budget
26//! should likely only be applied to the same host.
27//!
28//! # Classifiers
29//!
30//! A retry policy needs to be configured with a classifier that determines
31//! if a request should be retried. Knowledge of the destination server's
32//! behavior is required to make a safe classifier. **Requests should not be
33//! retried** if the server cannot safely handle the same request twice, or if
34//! it causes side effects.
35//!
36//! Some common properties to check include if the request method is
37//! idempotent, or if the response status code indicates a transient error.
38
39use std::sync::Arc;
40
41use http::Request;
42
43use crate::{
44 Body,
45 client::layer::retry::{Action, Classifier, ClassifyFn, ReqRep, ScopeFn, Scoped},
46};
47
48/// A retry policy.
49#[derive(Clone)]
50pub struct Policy {
51 pub(crate) budget: Option<f32>,
52 pub(crate) classifier: Classifier,
53 pub(crate) max_retries_per_request: u32,
54 pub(crate) scope: Scoped,
55}
56
57impl Policy {
58 /// Create a retry policy that will never retry any request.
59 ///
60 /// This is useful for disabling the `Client`s default behavior of retrying
61 /// protocol nacks.
62 #[inline]
63 pub fn never() -> Policy {
64 Self::scoped(|_| false).no_budget()
65 }
66
67 /// Create a retry policy scoped to requests for a specific host.
68 ///
69 /// This is a convenience method that creates a retry policy which only applies
70 /// to requests targeting the specified host. Requests to other hosts will not
71 /// be retried under this policy.
72 ///
73 /// # Arguments
74 /// * `host` - The hostname to match against request URIs (e.g., "api.example.com")
75 ///
76 /// # Example
77 /// ```rust
78 /// use hpx::retry::Policy;
79 ///
80 /// // Only retry requests to rust-lang.org
81 /// let policy = Policy::for_host("rust-lang.org");
82 /// ```
83 #[inline]
84 pub fn for_host<S>(host: S) -> Policy
85 where
86 S: for<'a> PartialEq<&'a str> + Send + Sync + 'static,
87 {
88 Self::scoped(move |req| {
89 req.uri()
90 .host()
91 .is_some_and(|request_host| host == request_host)
92 })
93 }
94
95 /// Create a scoped retry policy.
96 ///
97 /// For a more convenient constructor, see [`Policy::for_host()`].
98 #[inline]
99 fn scoped<F>(func: F) -> Policy
100 where
101 F: Fn(&Request<Body>) -> bool + Send + Sync + 'static,
102 {
103 Self {
104 budget: Some(0.2),
105 classifier: Classifier::Never,
106 max_retries_per_request: 2,
107 scope: Scoped::Dyn(Arc::new(ScopeFn(func))),
108 }
109 }
110
111 /// Set no retry budget.
112 ///
113 /// Sets that no budget will be enforced. This could also be considered
114 /// to be an infinite budget.
115 ///
116 /// This is NOT recommended. Disabling the budget can make your system more
117 /// susceptible to retry storms.
118 #[inline]
119 pub fn no_budget(mut self) -> Self {
120 self.budget = None;
121 self
122 }
123
124 /// Sets the max extra load the budget will allow.
125 ///
126 /// Think of the amount of requests your client generates, and how much
127 /// load that puts on the server. This option configures as a percentage
128 /// how much extra load is allowed via retries.
129 ///
130 /// For example, if you send 1,000 requests per second, setting a maximum
131 /// extra load value of `0.3` would allow 300 more requests per second
132 /// in retries. A value of `2.5` would allow 2,500 more requests.
133 ///
134 /// # Panics
135 ///
136 /// The `extra_percent` value must be within reasonable values for a
137 /// percentage. This method will panic if it is less than `0.0`, or greater
138 /// than `1000.0`.
139 #[inline]
140 pub fn max_extra_load(mut self, extra_percent: f32) -> Self {
141 assert!(extra_percent >= 0.0);
142 assert!(extra_percent <= 1000.0);
143 self.budget = Some(extra_percent);
144 self
145 }
146
147 /// Set the max retries allowed per request.
148 ///
149 /// For each logical (initial) request, only retry up to `max` times.
150 ///
151 /// This value is used in combination with a token budget that is applied
152 /// to all requests. Even if the budget would allow more requests, this
153 /// limit will prevent. Likewise, the budget may prevent retrying up to
154 /// `max` times. This setting prevents a single request from consuming
155 /// the entire budget.
156 ///
157 /// Default is currently 2 retries.
158 #[inline]
159 pub fn max_retries_per_request(mut self, max: u32) -> Self {
160 self.max_retries_per_request = max;
161 self
162 }
163
164 /// Provide a classifier to determine if a request should be retried.
165 ///
166 /// # Example
167 ///
168 /// ```rust
169 /// # fn with_policy(policy: hpx::retry::Policy) -> hpx::retry::Policy {
170 /// policy.classify_fn(|req_rep| match (req_rep.method(), req_rep.status()) {
171 /// (&http::Method::GET, Some(http::StatusCode::SERVICE_UNAVAILABLE)) => req_rep.retryable(),
172 /// _ => req_rep.success(),
173 /// })
174 /// # }
175 /// ```
176 #[inline]
177 pub fn classify_fn<F>(mut self, func: F) -> Self
178 where
179 F: Fn(ReqRep<'_>) -> Action + Send + Sync + 'static,
180 {
181 self.classifier = Classifier::Dyn(Arc::new(ClassifyFn(func)));
182 self
183 }
184}
185
186impl Default for Policy {
187 fn default() -> Self {
188 Self {
189 budget: None,
190 classifier: Classifier::ProtocolNacks,
191 max_retries_per_request: 2,
192 scope: Scoped::Unscoped,
193 }
194 }
195}