saluki_io/net/util/middleware/
retry_circuit_breaker.rs

1use std::{
2    fmt,
3    future::Future,
4    pin::Pin,
5    sync::{Arc, Mutex},
6    task::{ready, Context, Poll},
7};
8
9use futures::FutureExt as _;
10use pin_project_lite::pin_project;
11use tower::{retry::Policy, Layer, Service};
12use tracing::debug;
13
14/// An error from [`RetryCircuitBreaker`].
15#[derive(Debug)]
16pub enum Error<E, R> {
17    /// The inner service returned a readiness error or a final request error that will not be retried.
18    Service(E),
19
20    /// The inner service was called and completed, and `Policy::retry` returned `Some(backoff)`.
21    Retry(R),
22
23    /// The circuit breaker was already in backoff at call time, so the inner service was not called.
24    Open(R),
25}
26
27impl<E, R> std::error::Error for Error<E, R>
28where
29    E: std::error::Error + 'static,
30    R: fmt::Debug,
31{
32    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
33        match self {
34            Self::Service(e) => Some(e),
35            Self::Retry(_) | Self::Open(_) => None,
36        }
37    }
38}
39
40impl<E, R> fmt::Display for Error<E, R>
41where
42    E: fmt::Display,
43{
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::Service(e) => write!(f, "service error: {}", e),
47            Self::Retry(_) => write!(f, "request should be retried"),
48            Self::Open(_) => write!(f, "circuit breaker open"),
49        }
50    }
51}
52
53impl<E, R> PartialEq for Error<E, R>
54where
55    E: PartialEq,
56    R: PartialEq,
57{
58    fn eq(&self, other: &Self) -> bool {
59        match (self, other) {
60            (Self::Service(a), Self::Service(b)) => a == b,
61            (Self::Retry(a), Self::Retry(b)) => a == b,
62            (Self::Open(a), Self::Open(b)) => a == b,
63            _ => false,
64        }
65    }
66}
67
68pin_project! {
69    /// Response future for [`RetryCircuitBreaker`].
70    pub struct ResponseFuture<P, F, Request> {
71        state: Arc<Mutex<State<P>>>,
72        #[pin]
73        inner: Option<F>,
74        req: Option<Request>,
75    }
76}
77
78impl<P, F, T, E, Request> Future for ResponseFuture<P, F, Request>
79where
80    P: Policy<Request, T, E>,
81    P::Future: Send + 'static,
82    F: Future<Output = Result<T, E>>,
83{
84    type Output = Result<T, Error<E, Request>>;
85
86    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
87        // Our response future exists in two states: the circuit breaker was either closed or open when we created it.
88        //
89        // When the circuit breaker is open while creating the response future, there's no actual response future to
90        // call in this case. We simply store the original request and pass it back while indicating to the caller that
91        // the circuit breaker is open. Simple.
92        //
93        // When the circuit breaker is closed while creating the response future, this means we can proceed, and we
94        // generate a legitimate response future to poll. However, the retry policy may return `None` when trying to
95        // clone the request, which indicates the request actually isn't eligible to be retried at all. Thus, when we
96        // don't have an original request here, we just return the inner service's response as-is. When we _do_ have the
97        // original request, we utilize the retry policy to determine if it can be retried, and if so, potentially
98        // update our circuit breaker state based on what the retry policy tells us.
99
100        let this = self.project();
101        if let Some(inner) = this.inner.as_pin_mut() {
102            let mut result = ready!(inner.poll(cx));
103
104            let mut state = this.state.lock().unwrap();
105            match this.req.take() {
106                Some(mut req) => match state.policy.retry(&mut req, &mut result) {
107                    Some(backoff) => {
108                        // The policy has indicated that the request should be retried, so we need to open the circuit
109                        // breaker by setting the backoff future to use. Another request's retry decision may have
110                        // already beat us to the punch, though, so don't overwrite it if it's already set.
111                        if state.backoff.is_none() {
112                            debug!("no existing backoff future present, setting delay backoff");
113                            state.backoff = Some(backoff.boxed());
114                        }
115
116                        Poll::Ready(Err(Error::Retry(req)))
117                    }
118                    None => {
119                        debug!("request completed, no retry indicated");
120                        Poll::Ready(result.map_err(Error::Service))
121                    }
122                },
123                None => {
124                    debug!("request completed, but request not cloneable so returning response as-is");
125                    Poll::Ready(result.map_err(Error::Service))
126                }
127            }
128        } else {
129            debug!("circuit breaker open prior to call, returning error");
130            Poll::Ready(Err(Error::Open(
131                this.req.take().expect("response future polled after completion"),
132            )))
133        }
134    }
135}
136
137struct State<P> {
138    policy: P,
139    backoff: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
140}
141
142impl<P> std::fmt::Debug for State<P>
143where
144    P: std::fmt::Debug,
145{
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        let backoff = if self.backoff.is_some() { "set" } else { "unset" };
148        f.debug_struct("State")
149            .field("policy", &self.policy)
150            .field("backoff", &backoff)
151            .finish()
152    }
153}
154
155/// Wraps a service in a [circuit breaker][circuit_breaker] and signals when a request must be retried at a later time.
156///
157/// [circuit_breaker]: https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern
158pub struct RetryCircuitBreakerLayer<P> {
159    policy: P,
160}
161
162impl<P> RetryCircuitBreakerLayer<P> {
163    /// Creates a new [`RetryCircuitBreakerLayer`] with the given policy.
164    pub const fn new(policy: P) -> Self {
165        Self { policy }
166    }
167}
168
169impl<P, S> Layer<S> for RetryCircuitBreakerLayer<P>
170where
171    P: Clone,
172{
173    type Service = RetryCircuitBreaker<S, P>;
174
175    fn layer(&self, inner: S) -> Self::Service {
176        RetryCircuitBreaker::new(inner, self.policy.clone())
177    }
178}
179
180/// Wraps a service in a [circuit breaker][circuit_breaker] and signals when a request must be retried at a later time.
181///
182/// This circuit breaker implementation is specific to retrying requests. In many cases, a request can fail in two
183/// ways: unrecoverable errors, which shouldn't be retried, and recoverable errors, which should be retried after a
184/// some period of time. When a request can be retried, it may not be advantageous to wait for the given request to
185/// be retried successfully, as the request should perhaps be stored in a queue and retried at a later time,
186/// potentially to avoid applying backpressure to the client.
187///
188/// [`RetryCircuitBreaker`] provides this capability by separating the logic of determining whether or not a request
189/// should be retried from actually performing the retry itself. When a request leads to an unrecoverable error,
190/// that error is immediately passed back to the caller without affecting the circuit breaker state. However, when a
191/// recoverable error is encountered, the circuit breaker will signal to the caller that the request should be
192/// retried, and update its internal state to open the circuit breaker for a configurable period of time. Further
193/// requests to the circuit breaker will be rejected with an error (indicating the open state) until that period of
194/// time has passed.
195///
196/// [circuit_breaker]: https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern
197#[derive(Debug)]
198pub struct RetryCircuitBreaker<S, P> {
199    inner: S,
200    state: Arc<Mutex<State<P>>>,
201}
202
203impl<S, P> RetryCircuitBreaker<S, P> {
204    /// Creates a new [`RetryCircuitBreaker`].
205    pub fn new(inner: S, policy: P) -> Self {
206        Self {
207            inner,
208            state: Arc::new(Mutex::new(State { policy, backoff: None })),
209        }
210    }
211}
212
213impl<S, P, Request> Service<Request> for RetryCircuitBreaker<S, P>
214where
215    S: Service<Request>,
216    P: Policy<Request, S::Response, S::Error>,
217    P::Future: Send + 'static,
218{
219    type Response = S::Response;
220    type Error = Error<S::Error, Request>;
221    type Future = ResponseFuture<P, S::Future, Request>;
222
223    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
224        {
225            // Check if we're currently in a backoff state.
226            let mut state = self.state.lock().unwrap();
227            if let Some(backoff) = state.backoff.as_mut() {
228                ready!(backoff.as_mut().poll(cx));
229
230                debug!("circuit breaker backoff complete");
231
232                // The backoff future has completed, so we can reset the circuit breaker state.
233                state.backoff = None;
234            }
235        }
236
237        // Check the readiness of the inner service.
238        self.inner.poll_ready(cx).map_err(Error::Service)
239    }
240
241    fn call(&mut self, req: Request) -> Self::Future {
242        let response_state = Arc::clone(&self.state);
243
244        let mut state = self.state.lock().unwrap();
245        if state.backoff.is_some() {
246            ResponseFuture {
247                state: response_state,
248                inner: None,
249                req: Some(req),
250            }
251        } else {
252            // The circuit breaker is closed, so we can proceed with the request.
253            let cloned_req = state.policy.clone_request(&req);
254            let inner = self.inner.call(req);
255
256            ResponseFuture {
257                state: response_state,
258                inner: Some(inner),
259                req: cloned_req,
260            }
261        }
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use std::{
268        future::{ready, Ready},
269        time::Duration,
270    };
271
272    use tokio::time::Sleep;
273    use tokio_test::{assert_pending, assert_ready_ok};
274    use tower::{retry::Policy, ServiceExt as _};
275
276    use super::*;
277
278    const BACKOFF_DUR: Duration = Duration::from_secs(1);
279
280    #[derive(Clone, Debug, Eq, PartialEq)]
281    enum BasicRequest {
282        Ok(String),
283        Err(String),
284    }
285
286    impl BasicRequest {
287        fn success<S: AsRef<str>>(value: S) -> Self {
288            Self::Ok(value.as_ref().to_string())
289        }
290
291        fn failure<S: AsRef<str>>(value: S) -> Self {
292            Self::Err(value.as_ref().to_string())
293        }
294
295        fn as_service_response(&self) -> Result<String, Error<String, Self>> {
296            match self {
297                Self::Ok(value) => Ok(value.clone()),
298                Self::Err(value) => Err(Error::Service(value.clone())),
299            }
300        }
301
302        fn as_retry_response(&self) -> Result<String, Error<String, Self>> {
303            Err(Error::Retry(self.clone()))
304        }
305
306        fn as_open_response(&self) -> Result<String, Error<String, Self>> {
307            Err(Error::Open(self.clone()))
308        }
309    }
310
311    impl PartialEq<Result<String, String>> for BasicRequest {
312        fn eq(&self, other: &Result<String, String>) -> bool {
313            match self {
314                Self::Ok(value) => other.as_ref() == Ok(value),
315                Self::Err(value) => other.as_ref() == Err(value),
316            }
317        }
318    }
319
320    #[derive(Debug)]
321    struct LoopbackService;
322
323    impl Service<BasicRequest> for LoopbackService {
324        type Response = String;
325        type Error = String;
326        type Future = Ready<Result<String, String>>;
327
328        fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
329            Poll::Ready(Ok(()))
330        }
331
332        fn call(&mut self, req: BasicRequest) -> Self::Future {
333            let res = match req {
334                BasicRequest::Ok(value) => Ok(value),
335                BasicRequest::Err(value) => Err(value),
336            };
337            ready(res)
338        }
339    }
340
341    #[derive(Debug)]
342    struct CloneableTestRetryPolicy;
343
344    impl<Req, T, E> Policy<Req, T, E> for CloneableTestRetryPolicy
345    where
346        Req: Clone,
347    {
348        type Future = Sleep;
349
350        fn retry(&mut self, _: &mut Req, res: &mut Result<T, E>) -> Option<Self::Future> {
351            match res {
352                Ok(_) => None,
353                Err(_) => Some(tokio::time::sleep(BACKOFF_DUR)),
354            }
355        }
356
357        fn clone_request(&mut self, req: &Req) -> Option<Req> {
358            Some(req.clone())
359        }
360    }
361
362    #[derive(Debug)]
363    struct NonCloneableTestRetryPolicy;
364
365    impl<Req, T, E> Policy<Req, T, E> for NonCloneableTestRetryPolicy {
366        type Future = Sleep;
367
368        fn retry(&mut self, _: &mut Req, res: &mut Result<T, E>) -> Option<Self::Future> {
369            match res {
370                Ok(_) => None,
371                Err(_) => Some(tokio::time::sleep(BACKOFF_DUR)),
372            }
373        }
374
375        fn clone_request(&mut self, _: &Req) -> Option<Req> {
376            None
377        }
378    }
379
380    #[tokio::test(start_paused = true)]
381    async fn basic() {
382        let good_req = BasicRequest::success("good");
383        let bad_req = BasicRequest::failure("bad");
384
385        let mut circuit_breaker = RetryCircuitBreaker::new(LoopbackService, CloneableTestRetryPolicy);
386
387        // First request should succeed.
388        //
389        // We should see that it called through to the inner service.
390        let svc = circuit_breaker.ready().await.expect("should never fail to be ready");
391        let fut = svc.call(good_req.clone());
392        let result = fut.await;
393        assert_eq!(result, good_req.as_service_response());
394
395        // Second request should fail and should be retried.
396        //
397        // We should see that it called through to the inner service
398        let svc = circuit_breaker.ready().await.expect("should never fail to be ready");
399        let fut = svc.call(bad_req.clone());
400        let result = fut.await;
401        assert_eq!(result, bad_req.as_retry_response());
402
403        // When trying to make our third request, we should have to wait for the backoff duration before the service
404        // indicates that it's ready for another call.
405        let mut svc_fut = tokio_test::task::spawn(circuit_breaker.ready());
406        assert_pending!(svc_fut.poll());
407
408        // Advance time past the backoff duration, which should make our service ready.
409        tokio::time::advance(BACKOFF_DUR + Duration::from_millis(1)).await;
410        assert!(svc_fut.is_woken());
411        let svc = assert_ready_ok!(svc_fut.poll());
412
413        let fut = svc.call(good_req.clone());
414        let result = fut.await;
415        assert_eq!(result, good_req.as_service_response());
416
417        // Fourth request should succeed unimpeded since the breaker is closed again.
418        let svc = circuit_breaker.ready().await.expect("should never fail to be ready");
419        let fut = svc.call(good_req.clone());
420        let result = fut.await;
421        assert_eq!(result, good_req.as_service_response());
422    }
423
424    #[tokio::test]
425    async fn retry_policy_no_clone() {
426        let good_req = BasicRequest::success("good");
427        let bad_req = BasicRequest::failure("bad");
428
429        // First request should succeed.
430        //
431        // We should see that it called through to the inner service.
432        let mut circuit_breaker = RetryCircuitBreaker::new(LoopbackService, NonCloneableTestRetryPolicy);
433        let svc = circuit_breaker.ready().await.expect("should never fail to be ready");
434        let fut = svc.call(good_req.clone());
435        let result = fut.await;
436        assert_eq!(result, good_req.as_service_response());
437
438        // Second request should fail and should be a service error, because without being able to clone the request, it
439        // can't be retried anyways.
440        let mut circuit_breaker = RetryCircuitBreaker::new(LoopbackService, NonCloneableTestRetryPolicy);
441        let svc = circuit_breaker.ready().await.expect("should never fail to be ready");
442        let fut = svc.call(bad_req.clone());
443        let result = fut.await;
444        assert_eq!(result, bad_req.as_service_response());
445    }
446
447    #[tokio::test(start_paused = true)]
448    async fn concurrent_calls_can_advance() {
449        let good_req = BasicRequest::success("good");
450        let bad_req = BasicRequest::failure("bad");
451
452        let mut circuit_breaker = RetryCircuitBreaker::new(LoopbackService, CloneableTestRetryPolicy);
453
454        // First request should succeed. This is just a warmup.
455        let svc = circuit_breaker.ready().await.expect("should never fail to be ready");
456        let fut = svc.call(good_req.clone());
457        let result = fut.await;
458        assert_eq!(result, good_req.as_service_response());
459
460        // Now we'll create two calls -- one that should fail and one that succeed -- but won't poll them until both are
461        // created. This simulates two concurrent calls happening, and what we want to show is that the circuit breaker
462        // should only mark itself as open to _new_ calls after it the state changes to open, and should not affect
463        // running requests.
464        let svc = circuit_breaker.ready().await.expect("should never fail to be ready");
465        let bad_fut = svc.call(bad_req.clone());
466
467        let svc = circuit_breaker.ready().await.expect("should never fail to be ready");
468        let good_fut = svc.call(good_req.clone());
469
470        let bad_result = bad_fut.await;
471        assert_eq!(bad_result, bad_req.as_retry_response());
472
473        let good_result = good_fut.await;
474        assert_eq!(good_result, good_req.as_service_response());
475
476        // Now we'll go to make a fourth request, and we'll manually check the readiness of the service to ensure that
477        // we're now in a backoff state.
478        let mut svc_fut = tokio_test::task::spawn(circuit_breaker.ready());
479        assert_pending!(svc_fut.poll());
480    }
481
482    #[tokio::test(start_paused = true)]
483    async fn breaker_open_between_ready_and_call() {
484        let good_req = BasicRequest::success("good");
485        let bad_req = BasicRequest::failure("bad");
486
487        let mut circuit_breaker = RetryCircuitBreaker::new(LoopbackService, CloneableTestRetryPolicy);
488
489        // We'll create two calls -- one that should fail and one that succeed -- but order their creation / polling such that the
490        // bad request updates the breaker state to be open _before_ we create the good request. This is to exercise
491        // that even though the service may report itself as ready, an in-flight request that completes and ultimately
492        // changes the breaker state to open should cause subsequent calls to immediately fail.
493        let svc = circuit_breaker.ready().await.expect("should never fail to be ready");
494        let bad_fut = svc.call(bad_req.clone());
495
496        // We're just making sure here that the service is ready to accept another call, but we're not creating that
497        // call yet.
498        let svc = circuit_breaker.ready().await.expect("should never fail to be ready");
499
500        // Run our bad request first and ensure it reports that the completed request should be retried.
501        let bad_result = bad_fut.await;
502        assert_eq!(bad_result, bad_req.as_retry_response());
503
504        // Now _create_ the good request and ensure that it fails with an open error without reaching the inner service.
505        let good_fut = svc.call(good_req.clone());
506        let good_result = good_fut.await;
507        assert_eq!(good_result, good_req.as_open_response());
508    }
509}