1use super::{Policy, PolicyResult, RetryBody};
8use crate::{Request, Response};
9use rama_core::extensions::{Extension, ExtensionsRef};
10use rama_core::telemetry::tracing;
11use rama_utils::backoff::Backoff;
12
13#[derive(Debug, Clone, Default, Extension)]
14#[extension(tags(http))]
15#[non_exhaustive]
22pub struct DoNotRetry;
23
24#[derive(Debug, Clone)]
33pub struct ManagedPolicy<B = Undefined, C = Undefined, R = Undefined> {
34 backoff: B,
35 clone: C,
36 retry: R,
37}
38
39impl<B, C, R, Response, Error> Policy<Response, Error> for ManagedPolicy<B, C, R>
40where
41 B: Backoff,
42 C: CloneInput,
43 R: RetryRule<Request<RetryBody>, Response, Error>,
44 Response: Send + 'static,
45 Error: Send + 'static,
46{
47 async fn retry(
48 &self,
49 req: Request<RetryBody>,
50 result: Result<Response, Error>,
51 ) -> PolicyResult<Response, Error> {
52 if req.extensions().get_ref::<DoNotRetry>().is_some() {
53 return PolicyResult::Abort(result);
55 }
56
57 let (req, result, retry) = self.retry.retry(req, result).await;
58 if retry && self.backoff.next_backoff().await {
59 PolicyResult::Retry { req }
60 } else {
61 self.backoff.reset().await;
62 PolicyResult::Abort(result)
63 }
64 }
65
66 fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
67 if req.extensions().get_ref::<DoNotRetry>().is_some() {
68 None
69 } else {
70 self.clone.clone_input(req)
71 }
72 }
73}
74
75impl Default for ManagedPolicy<Undefined, Undefined, Undefined> {
76 fn default() -> Self {
77 Self {
78 backoff: Undefined,
79 clone: Undefined,
80 retry: Undefined,
81 }
82 }
83}
84
85impl<F> ManagedPolicy<Undefined, Undefined, F> {
86 #[inline]
91 pub fn new(retry: F) -> Self {
92 ManagedPolicy::default().with_retry(retry)
93 }
94}
95
96impl<C, R> ManagedPolicy<Undefined, C, R> {
97 pub fn with_backoff<B>(self, backoff: B) -> ManagedPolicy<B, C, R> {
99 ManagedPolicy {
100 backoff,
101 clone: self.clone,
102 retry: self.retry,
103 }
104 }
105}
106
107impl<B, R> ManagedPolicy<B, Undefined, R> {
108 pub fn with_clone<C>(self, clone: C) -> ManagedPolicy<B, C, R> {
111 ManagedPolicy {
112 backoff: self.backoff,
113 clone,
114 retry: self.retry,
115 }
116 }
117}
118
119impl<B, C> ManagedPolicy<B, C, Undefined> {
120 pub fn with_retry<R>(self, retry: R) -> ManagedPolicy<B, C, R> {
123 ManagedPolicy {
124 backoff: self.backoff,
125 clone: self.clone,
126 retry,
127 }
128 }
129}
130
131pub trait RetryRule<Request, R, E>:
134 private::Sealed<(Request, R, E)> + Send + Sync + 'static
135{
136 fn retry(
138 &self,
139 request: Request,
140 result: Result<R, E>,
141 ) -> impl Future<Output = (Request, Result<R, E>, bool)> + Send + '_;
142}
143
144impl<Request, Body, E> RetryRule<Request, Response<Body>, E> for Undefined
145where
146 E: std::fmt::Debug + Send + Sync + 'static,
147 Body: Send + 'static,
148 Request: ExtensionsRef + Send + 'static,
149{
150 async fn retry(
151 &self,
152 request: Request,
153 result: Result<Response<Body>, E>,
154 ) -> (Request, Result<Response<Body>, E>, bool) {
155 match &result {
156 Ok(response) => {
157 let status = response.status();
158 if status.is_server_error() {
159 tracing::debug!(
160 "retrying server error http status code: {status} ({})",
161 status.as_u16()
162 );
163 (request, result, true)
164 } else {
165 (request, result, false)
166 }
167 }
168 Err(error) => {
169 tracing::debug!("retrying error: {:?}", error);
170 (request, result, true)
171 }
172 }
173 }
174}
175
176impl<F, Fut, Request, R, E> RetryRule<Request, R, E> for F
177where
178 F: Fn(Request, Result<R, E>) -> Fut + Send + Sync + 'static,
179 Fut: Future<Output = (Request, Result<R, E>, bool)> + Send + 'static,
180 Request: Send + 'static,
181 R: Send + 'static,
182 E: Send + Sync + 'static,
183{
184 async fn retry(&self, request: Request, result: Result<R, E>) -> (Request, Result<R, E>, bool) {
185 self(request, result).await
186 }
187}
188
189pub trait CloneInput: private::Sealed<()> + Send + Sync + 'static {
192 fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>>;
198}
199
200impl CloneInput for Undefined {
201 fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
202 Some(req.clone())
203 }
204}
205
206impl<F> CloneInput for F
207where
208 F: Fn(&Request<RetryBody>) -> Option<Request<RetryBody>> + Send + Sync + 'static,
209{
210 fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
211 self(req)
212 }
213}
214
215#[derive(Debug, Clone)]
216#[non_exhaustive]
217pub struct Undefined;
221
222impl std::fmt::Display for Undefined {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 write!(f, "Undefined")
225 }
226}
227
228impl Backoff for Undefined {
229 async fn next_backoff(&self) -> bool {
230 true
231 }
232
233 async fn reset(&self) {}
234}
235
236mod private {
237 use super::*;
238
239 pub trait Sealed<S> {}
240
241 impl<S> Sealed<S> for Undefined {}
242 impl<F> Sealed<()> for F where
243 F: Fn(&Request<RetryBody>) -> Option<Request<RetryBody>> + Send + Sync + 'static
244 {
245 }
246 impl<F, Fut, Request, R, E> Sealed<(Request, R, E)> for F
247 where
248 F: Fn(Request, Result<R, E>) -> Fut + Send + Sync + 'static,
249 Fut: Future<Output = (Request, Result<R, E>, bool)> + Send + 'static,
250 {
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::{StatusCode, service::web::response::IntoResponse};
258 use rama_core::extensions::ExtensionsRef;
259 use rama_utils::{backoff::ExponentialBackoff, rng::HasherRng};
260 use std::time::Duration;
261
262 fn assert_clone_input_none(req: &Request<RetryBody>, policy: &impl Policy<Response, ()>) {
263 assert!(policy.clone_input(req).is_none());
264 }
265
266 fn assert_clone_input_some(req: &Request<RetryBody>, policy: &impl Policy<Response, ()>) {
267 assert!(policy.clone_input(req).is_some());
268 }
269
270 async fn assert_retry(
271 req: Request<RetryBody>,
272 result: Result<Response, ()>,
273 policy: &impl Policy<Response, ()>,
274 ) {
275 match policy.retry(req, result).await {
276 PolicyResult::Retry { .. } => (),
277 PolicyResult::Abort(_) => panic!("expected retry"),
278 };
279 }
280
281 async fn assert_abort(
282 req: Request<RetryBody>,
283 result: Result<Response, ()>,
284 policy: &impl Policy<Response, ()>,
285 ) {
286 match policy.retry(req, result).await {
287 PolicyResult::Retry { .. } => panic!("expected abort"),
288 PolicyResult::Abort(_) => (),
289 };
290 }
291
292 #[tokio::test]
293 async fn managed_policy_default() {
294 let request = Request::builder()
295 .method("GET")
296 .uri("http://example.com")
297 .body(RetryBody::empty())
298 .unwrap();
299
300 let policy = ManagedPolicy::default();
301
302 assert_clone_input_some(&request, &policy);
303
304 assert_abort(request.clone(), Ok(StatusCode::OK.into_response()), &policy).await;
306
307 assert_retry(
309 request.clone(),
310 Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
311 &policy,
312 )
313 .await;
314
315 assert_retry(request, Err(()), &policy).await;
317 }
318
319 #[tokio::test]
320 async fn managed_policy_default_do_not_retry() {
321 let req = Request::builder()
322 .method("GET")
323 .uri("http://example.com")
324 .body(RetryBody::empty())
325 .unwrap();
326
327 let policy = ManagedPolicy::default();
328
329 req.extensions().insert(DoNotRetry);
330
331 assert_clone_input_none(&req, &policy);
332
333 assert_abort(req.clone(), Ok(StatusCode::OK.into_response()), &policy).await;
335
336 assert_abort(
338 req.clone(),
339 Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
340 &policy,
341 )
342 .await;
343
344 assert_abort(req, Err(()), &policy).await;
346 }
347
348 #[tokio::test]
349 async fn test_policy_custom_clone_fn() {
350 let req = Request::builder()
351 .method("GET")
352 .uri("http://example.com")
353 .body(RetryBody::empty())
354 .unwrap();
355
356 fn clone_fn(_: &Request<RetryBody>) -> Option<Request<RetryBody>> {
357 None
358 }
359
360 let policy = ManagedPolicy::default().with_clone(clone_fn);
361
362 assert_clone_input_none(&req, &policy);
363
364 assert_abort(req, Ok(StatusCode::OK.into_response()), &policy).await;
366 }
367
368 #[tokio::test]
369 async fn test_policy_custom_retry_fn() {
370 let req = Request::builder()
371 .method("GET")
372 .uri("http://example.com")
373 .body(RetryBody::empty())
374 .unwrap();
375
376 async fn retry_fn<Body, R, E>(
377 request: Request<Body>,
378 result: Result<R, E>,
379 ) -> (Request<Body>, Result<R, E>, bool) {
380 match result {
381 Ok(_) => (request, result, false),
382 Err(_) => (request, result, true),
383 }
384 }
385
386 let policy = ManagedPolicy::new(retry_fn);
387
388 assert_clone_input_some(&req, &policy);
390
391 assert_abort(req.clone(), Ok(StatusCode::OK.into_response()), &policy).await;
393 assert_abort(
394 req.clone(),
395 Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
396 &policy,
397 )
398 .await;
399 assert_retry(req, Err(()), &policy).await;
400 }
401
402 #[tokio::test]
403 async fn test_policy_fully_custom() {
404 let req = Request::builder()
405 .method("GET")
406 .uri("http://example.com")
407 .body(RetryBody::empty())
408 .unwrap();
409
410 fn clone_fn(_: &Request<RetryBody>) -> Option<Request<RetryBody>> {
411 None
412 }
413
414 async fn retry_fn<Body, R, E>(
415 req: Request<Body>,
416 result: Result<R, E>,
417 ) -> (Request<Body>, Result<R, E>, bool) {
418 match result {
419 Ok(_) => (req, result, false),
420 Err(_) => (req, result, true),
421 }
422 }
423
424 let backoff = ExponentialBackoff::new(
425 Duration::from_millis(1),
426 Duration::from_millis(5),
427 0.1,
428 HasherRng::default,
429 )
430 .unwrap();
431
432 let policy = ManagedPolicy::default()
433 .with_backoff(backoff)
434 .with_clone(clone_fn)
435 .with_retry(retry_fn);
436
437 assert_clone_input_none(&req, &policy);
438
439 assert_abort(req.clone(), Ok(StatusCode::OK.into_response()), &policy).await;
441 assert_abort(
442 req.clone(),
443 Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
444 &policy,
445 )
446 .await;
447 assert_retry(req, Err(()), &policy).await;
448 }
449}