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}