Skip to main content

sark_client/connector/
retry.rs

1use http::Method;
2
3use crate::connector::error::Error;
4
5#[derive(Clone, Copy, Debug)]
6pub struct RetryPolicy {
7    pub idempotent_attempts: u8,
8    pub non_idempotent_attempts: u8,
9    pub reconnect_on_stale_non_idempotent: bool,
10}
11
12impl Default for RetryPolicy {
13    fn default() -> Self {
14        Self {
15            idempotent_attempts: 3,
16            non_idempotent_attempts: 2,
17            reconnect_on_stale_non_idempotent: true,
18        }
19    }
20}
21
22impl RetryPolicy {
23    pub fn is_idempotent(method: &Method) -> bool {
24        matches!(
25            *method,
26            Method::GET | Method::HEAD | Method::OPTIONS | Method::PUT | Method::DELETE
27        )
28    }
29
30    pub fn attempts(&self, method: &Method) -> u32 {
31        if Self::is_idempotent(method) {
32            self.idempotent_attempts.max(1) as u32
33        } else {
34            self.non_idempotent_attempts.max(1) as u32
35        }
36    }
37
38    pub fn should_retry(&self, method: &Method, err: &Error) -> bool {
39        if Self::is_idempotent(method) {
40            return matches!(err, Error::Closed | Error::NotConnected | Error::Timeout);
41        }
42        match err {
43            Error::NotConnected => true,
44            Error::Closed => self.reconnect_on_stale_non_idempotent,
45            _ => false,
46        }
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn idempotent_retries_all_transient_errors() {
56        let p = RetryPolicy::default();
57        for err in [Error::Closed, Error::NotConnected, Error::Timeout] {
58            assert!(p.should_retry(&Method::GET, &err));
59        }
60        assert!(!p.should_retry(&Method::GET, &Error::Parse("x".into())));
61        assert!(!p.should_retry(&Method::GET, &Error::Backpressure));
62    }
63
64    #[test]
65    fn non_idempotent_never_retries_on_timeout() {
66        let p = RetryPolicy::default();
67        assert!(!p.should_retry(&Method::POST, &Error::Timeout));
68        assert!(!p.should_retry(&Method::PATCH, &Error::Timeout));
69    }
70
71    #[test]
72    fn non_idempotent_retries_not_connected() {
73        let p = RetryPolicy::default();
74        assert!(p.should_retry(&Method::POST, &Error::NotConnected));
75    }
76
77    #[test]
78    fn non_idempotent_closed_follows_reconnect_flag() {
79        let mut p = RetryPolicy {
80            reconnect_on_stale_non_idempotent: true,
81            ..Default::default()
82        };
83        assert!(p.should_retry(&Method::POST, &Error::Closed));
84        p.reconnect_on_stale_non_idempotent = false;
85        assert!(!p.should_retry(&Method::POST, &Error::Closed));
86    }
87
88    #[test]
89    fn patch_is_non_idempotent() {
90        assert!(!RetryPolicy::is_idempotent(&Method::POST));
91        assert!(!RetryPolicy::is_idempotent(&Method::PATCH));
92        assert!(RetryPolicy::is_idempotent(&Method::PUT));
93        assert!(RetryPolicy::is_idempotent(&Method::DELETE));
94    }
95}