1use crate::{Request, StreamingBody, body::util::BodyExt};
37use rama_core::Service;
38use rama_core::error::BoxError;
39use rama_utils::macros::define_inner_service_accessors;
40
41mod layer;
42mod policy;
43
44mod body;
45#[doc(inline)]
46pub use body::RetryBody;
47
48pub mod managed;
49pub use managed::ManagedPolicy;
50
51#[cfg(test)]
52mod tests;
53
54pub use self::layer::RetryLayer;
55pub use self::policy::{Policy, PolicyResult};
56
57#[derive(Debug, Clone)]
61pub struct Retry<P, S> {
62 policy: P,
63 inner: S,
64}
65
66impl<P, S> Retry<P, S> {
69 pub const fn new(policy: P, service: S) -> Self {
71 Self {
72 policy,
73 inner: service,
74 }
75 }
76
77 define_inner_service_accessors!();
78}
79
80#[derive(Debug)]
81pub struct RetryError {
83 kind: RetryErrorKind,
84 inner: Option<BoxError>,
85}
86
87#[derive(Debug)]
88enum RetryErrorKind {
89 BodyConsume,
90 Service,
91}
92
93impl std::fmt::Display for RetryError {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match &self.inner {
96 Some(inner) => write!(f, "{}: {}", self.kind, inner),
97 None => write!(f, "{}", self.kind),
98 }
99 }
100}
101
102impl std::fmt::Display for RetryErrorKind {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 match self {
105 Self::BodyConsume => write!(f, "failed to consume body"),
106 Self::Service => write!(f, "service error"),
107 }
108 }
109}
110
111impl std::error::Error for RetryError {
112 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
113 self.inner.as_ref().and_then(|e| e.source())
114 }
115}
116
117impl<P, S, Body> Service<Request<Body>> for Retry<P, S>
118where
119 P: Policy<S::Output, S::Error>,
120 S: Service<Request<RetryBody>, Error: Into<BoxError>>,
121 Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Send + 'static,
122{
123 type Output = S::Output;
124 type Error = RetryError;
125
126 async fn serve(&self, request: Request<Body>) -> Result<Self::Output, Self::Error> {
127 let (parts, body) = request.into_parts();
129 let body = body.collect().await.map_err(|e| RetryError {
130 kind: RetryErrorKind::BodyConsume,
131 inner: Some(e.into()),
132 })?;
133 let body = RetryBody::new(body.to_bytes());
134 let mut request = Request::from_parts(parts, body);
135
136 let mut cloned = self.policy.clone_input(&request);
137
138 let parent_ext = request.fork_extensions_in_place();
143 loop {
144 let resp = self.inner.serve(request).await;
145 match cloned.take() {
146 Some(cloned_req) => {
147 let cloned_req = match self.policy.retry(cloned_req, resp).await {
148 PolicyResult::Abort(result) => {
149 return result.map_err(|e| RetryError {
150 kind: RetryErrorKind::Service,
151 inner: Some(e.into()),
152 });
153 }
154 PolicyResult::Retry { req } => req,
155 };
156
157 request = cloned_req;
158 request.set_extensions(parent_ext.clone());
162 cloned = self.policy.clone_input(&request);
163 request.set_extensions(parent_ext.fork());
164 }
165 None => {
167 return resp.map_err(|e| RetryError {
168 kind: RetryErrorKind::Service,
169 inner: Some(e.into()),
170 });
171 }
172 }
173 }
174 }
175}
176
177#[cfg(test)]
178mod test {
179 use super::*;
180 use crate::{
181 BodyExtractExt, Response, StatusCode, layer::retry::managed::DoNotRetry,
182 service::web::response::IntoResponse,
183 };
184 use rama_core::{
185 Layer,
186 error::BoxErrorExt,
187 extensions::{Extension, Extensions, ExtensionsRef},
188 service::service_fn,
189 };
190 use rama_utils::{backoff::ExponentialBackoff, rng::HasherRng};
191 use std::{sync::atomic::AtomicUsize, time::Duration};
192
193 #[tokio::test]
194 async fn test_service_with_managed_retry() {
195 let backoff = ExponentialBackoff::new(
196 Duration::from_millis(1),
197 Duration::from_millis(5),
198 0.1,
199 HasherRng::default,
200 )
201 .unwrap();
202
203 #[derive(Debug, Extension)]
204 struct State {
205 retry_counter: AtomicUsize,
206 }
207
208 async fn retry<Body, E>(
209 req: Request<Body>,
210 result: Result<Response, E>,
211 ) -> (Request<Body>, Result<Response, E>, bool) {
212 if req.extensions().contains::<DoNotRetry>() {
213 panic!("unexpected retry: should be disabled");
214 }
215
216 if let Ok(ref res) = result {
217 if res.status().is_server_error() {
218 req.extensions()
219 .get_ref::<State>()
220 .unwrap()
221 .retry_counter
222 .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
223 (req, result, true)
224 } else {
225 (req, result, false)
226 }
227 } else {
228 req.extensions()
229 .get_ref::<State>()
230 .unwrap()
231 .retry_counter
232 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
233 (req, result, true)
234 }
235 }
236
237 let retry_policy = ManagedPolicy::new(retry).with_backoff(backoff);
238
239 let service = RetryLayer::new(retry_policy).into_layer(service_fn(
240 async |req: Request<RetryBody>| {
241 let txt = req.try_into_string().await.unwrap();
242 match txt.as_str() {
243 "internal" => Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
244 "error" => Err(BoxError::from_static_str("custom error")),
245 _ => Ok(txt.into_response()),
246 }
247 },
248 ));
249
250 fn request(s: &'static str) -> Request {
251 Request::builder().body(s.into()).unwrap()
252 }
253
254 fn extensions() -> Extensions {
255 let extensions = Extensions::new();
256 extensions.insert(State {
257 retry_counter: AtomicUsize::new(0),
258 });
259 extensions
260 }
261
262 fn do_not_retry_extensions() -> Extensions {
263 let extensions = extensions();
264 extensions.insert(DoNotRetry::default());
265 extensions
266 }
267
268 async fn assert_serve_ok<E: std::fmt::Debug>(
269 msg: &'static str,
270 input: &'static str,
271 output: &'static str,
272 extensions: Extensions,
273 retried: bool,
274 service: &impl Service<Request, Output = Response, Error = E>,
275 ) {
276 let state = extensions.get_arc::<State>().unwrap();
277
278 let request = request(input);
279 request.extensions().extend(&extensions);
280
281 let fut = service.serve(request);
282 let res = fut.await.unwrap();
283
284 let body = res.try_into_string().await.unwrap();
285 assert_eq!(body, output, "{msg}");
286 if retried {
287 assert!(
288 state
289 .retry_counter
290 .load(std::sync::atomic::Ordering::Acquire)
291 > 0,
292 "{msg}"
293 );
294 } else {
295 assert_eq!(
296 state
297 .retry_counter
298 .load(std::sync::atomic::Ordering::Acquire),
299 0,
300 "{msg}"
301 );
302 }
303 }
304
305 async fn assert_serve_err<E: std::fmt::Debug>(
306 msg: &'static str,
307 input: &'static str,
308 extensions: Extensions,
309 retried: bool,
310 service: &impl Service<Request, Output = Response, Error = E>,
311 ) {
312 let state = extensions.get_arc::<State>().unwrap();
313
314 let request = request(input);
315 request.extensions().extend(&extensions);
316
317 let fut = service.serve(request);
318 let res = fut.await;
319
320 assert!(res.is_err(), "{msg}");
321 if retried {
322 assert!(
323 state
324 .retry_counter
325 .load(std::sync::atomic::Ordering::Acquire)
326 > 0,
327 "{msg}"
328 );
329 } else {
330 assert_eq!(
331 state
332 .retry_counter
333 .load(std::sync::atomic::Ordering::Acquire),
334 0,
335 "{msg}"
336 )
337 }
338 }
339
340 assert_serve_ok(
341 "ok response should be aborted as response without retry",
342 "hello",
343 "hello",
344 extensions(),
345 false,
346 &service,
347 )
348 .await;
349 assert_serve_ok(
350 "internal will trigger 500 with a retry",
351 "internal",
352 "",
353 extensions(),
354 true,
355 &service,
356 )
357 .await;
358 assert_serve_err(
359 "error will trigger an actual non-http error with a retry",
360 "error",
361 extensions(),
362 true,
363 &service,
364 )
365 .await;
366
367 assert_serve_ok(
368 "normally internal will trigger a 500 with retry, but using DoNotRetry will disable retrying",
369 "internal",
370 "",
371 do_not_retry_extensions(),
372 false,
373 &service,
374 ).await;
375 }
376
377 #[tokio::test]
378 async fn inner_layer_do_not_retry_insert_does_not_stop_retries() {
379 use std::sync::Arc;
380 use std::sync::atomic::{AtomicUsize, Ordering};
381
382 async fn retry_on_server_error<Body, E>(
383 req: Request<Body>,
384 result: Result<Response, E>,
385 ) -> (Request<Body>, Result<Response, E>, bool) {
386 let retry = matches!(&result, Ok(res) if res.status().is_server_error());
387 (req, result, retry)
388 }
389
390 let backoff = ExponentialBackoff::new(
391 Duration::from_millis(1),
392 Duration::from_millis(5),
393 0.1,
394 HasherRng::default,
395 )
396 .unwrap();
397
398 let calls = Arc::new(AtomicUsize::new(0));
399 let inner_calls = calls.clone();
400 let service =
401 RetryLayer::new(ManagedPolicy::new(retry_on_server_error).with_backoff(backoff))
402 .into_layer(service_fn(move |req: Request<RetryBody>| {
403 let inner_calls = inner_calls.clone();
404 async move {
405 let n = inner_calls.fetch_add(1, Ordering::SeqCst);
406 req.extensions().insert(DoNotRetry::default());
410 if n == 0 {
411 Ok::<_, BoxError>(StatusCode::INTERNAL_SERVER_ERROR.into_response())
412 } else {
413 Ok(StatusCode::OK.into_response())
414 }
415 }
416 }));
417
418 let request: Request = Request::builder().body("x".into()).unwrap();
419 let res = service.serve(request).await.unwrap();
420 assert_eq!(res.status(), StatusCode::OK);
421 assert_eq!(
422 calls.load(Ordering::SeqCst),
423 2,
424 "an inner-layer DoNotRetry insert must not suppress the retry"
425 );
426 }
427
428 #[tokio::test]
429 async fn fresh_policy_requests_keep_the_caller_extension_view() {
430 use std::sync::Arc;
431 use std::sync::atomic::{AtomicUsize, Ordering};
432
433 #[derive(Debug, Extension)]
434 struct CallerState(AtomicUsize);
435 #[derive(Debug, Extension)]
436 struct AttemptOnly;
437
438 #[derive(Clone)]
439 struct FreshRequestPolicy;
440
441 impl Policy<Response, BoxError> for FreshRequestPolicy {
442 async fn retry(
443 &self,
444 req: Request<RetryBody>,
445 result: Result<Response, BoxError>,
446 ) -> PolicyResult<Response, BoxError> {
447 assert!(!req.extensions().contains::<AttemptOnly>());
448 let call = req
449 .extensions()
450 .get_ref::<CallerState>()
451 .expect("every policy round sees the caller store")
452 .0
453 .fetch_add(1, Ordering::SeqCst);
454 if call < 2 {
455 PolicyResult::Retry {
456 req: Request::new(RetryBody::empty()),
457 }
458 } else {
459 PolicyResult::Abort(result)
460 }
461 }
462
463 fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
464 Some(req.clone())
465 }
466 }
467
468 let attempts = Arc::new(AtomicUsize::new(0));
469 let inner_attempts = attempts.clone();
470 let service = RetryLayer::new(FreshRequestPolicy).into_layer(service_fn(
471 move |req: Request<RetryBody>| {
472 let inner_attempts = inner_attempts.clone();
473 async move {
474 assert!(req.extensions().contains::<CallerState>());
475 req.extensions().insert(AttemptOnly);
476 inner_attempts.fetch_add(1, Ordering::SeqCst);
477 Ok::<_, BoxError>(StatusCode::INTERNAL_SERVER_ERROR.into_response())
478 }
479 },
480 ));
481
482 let caller_extensions = Extensions::new();
483 caller_extensions.insert(CallerState(AtomicUsize::new(0)));
484 let mut request = Request::new(RetryBody::empty());
485 request.set_extensions(caller_extensions.clone());
486
487 let response = service.serve(request).await.unwrap();
488 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
489 assert_eq!(attempts.load(Ordering::SeqCst), 3);
490 assert_eq!(
491 caller_extensions
492 .get_ref::<CallerState>()
493 .unwrap()
494 .0
495 .load(Ordering::SeqCst),
496 3
497 );
498 assert!(!caller_extensions.contains::<AttemptOnly>());
499 }
500}