soda_pool/
retry.rs

1use std::time::Duration;
2
3/// Retry policy for the request.
4///
5/// This trait is used to determine whether a request should be retried or not
6/// based on the error returned by the server and number of attempts. It also
7/// provides information about the status of the server and the time to wait
8/// before retrying the request.
9///
10/// # Note
11///
12/// This trait is designed to be used by clients generated by [`soda-pool-build`] crate.
13/// While it can be useful in other cases too, it is not used directly anywhere in this crate.
14///
15/// # Warning
16///
17/// If there are no more alive connections in the pool, the request will not be
18/// retried and the last error will be returned to the caller.
19/// **The retry policy cannot be used to override this behavior.**
20pub trait RetryPolicy {
21    /// Called to determine the status of the server and whether the request
22    /// should be retried or not.
23    fn should_retry(err: &tonic::Status, tries: u8) -> RetryPolicyResult;
24}
25
26/// Status of the server.
27#[derive(Debug, PartialEq, Clone, Copy, Eq, PartialOrd, Ord, Hash)]
28pub enum ServerStatus {
29    /// The server should be treated as alive.
30    Alive,
31
32    /// The server should be treated as dead.
33    Dead,
34}
35
36/// Retry time of the failed request.
37#[derive(Debug, PartialEq, Clone, Copy, Eq, PartialOrd, Ord, Hash)]
38pub enum RetryTime {
39    /// Do not retry the request.
40    DoNotRetry,
41
42    /// Retry the request immediately.
43    Immediately,
44
45    /// Retry the request after a certain delay.
46    After(Duration),
47}
48
49/// Result of the retry policy.
50///
51/// [`RetryPolicy::should_retry`] returns this type to indicate the status of
52/// the server and the time to wait before retrying the request.
53pub type RetryPolicyResult = (ServerStatus, RetryTime);
54
55/// Default retry policy.
56///
57/// This policy retries the request immediately and marks the server as dead if
58/// it seems to be a network error or otherwise a problem originating from the
59/// client library rather than the server. I also don't have a limit on the
60/// number of retries and will continue as long as there is still an alive
61/// connection remaining.
62#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
63pub struct DefaultRetryPolicy;
64
65impl RetryPolicy for DefaultRetryPolicy {
66    fn should_retry(err: &tonic::Status, _tries: u8) -> RetryPolicyResult {
67        // Initial tests suggest that source of the error is set only when it comes
68        // from the client library (e.g. connection refused) and not the server.
69        if std::error::Error::source(err).is_some() {
70            (ServerStatus::Dead, RetryTime::Immediately)
71        } else {
72            (ServerStatus::Alive, RetryTime::DoNotRetry)
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use tonic::Status;
81
82    #[test]
83    fn test_default_retry_policy_alive() {
84        let err = Status::new(tonic::Code::Unknown, "test error");
85        let result = DefaultRetryPolicy::should_retry(&err, 1);
86        assert_eq!(result, (ServerStatus::Alive, RetryTime::DoNotRetry));
87    }
88
89    #[test]
90    fn test_default_retry_policy_dead() {
91        #[derive(Debug)]
92        struct TestError;
93        impl std::fmt::Display for TestError {
94            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95                write!(f, "Test error")
96            }
97        }
98        impl std::error::Error for TestError {}
99
100        let err = Status::from_error(Box::new(TestError));
101        let result = DefaultRetryPolicy::should_retry(&err, 1);
102        assert_eq!(result, (ServerStatus::Dead, RetryTime::Immediately));
103    }
104}