Skip to main content

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}