saluki_io/net/util/retry/policy/
rolling_exponential.rs1use std::sync::{
2 atomic::{
3 AtomicU32,
4 Ordering::{AcqRel, Relaxed},
5 },
6 Arc,
7};
8
9use tokio::time::{sleep, Sleep};
10use tower::retry::Policy;
11use tracing::debug;
12
13use crate::net::util::retry::{
14 classifier::RetryClassifier,
15 lifecycle::{DefaultDebugRetryLifecycle, RetryLifecycle},
16 ExponentialBackoff,
17};
18
19#[derive(Clone)]
44pub struct RollingExponentialBackoffRetryPolicy<C, L = DefaultDebugRetryLifecycle> {
45 classifier: C,
46 retry_lifecycle: L,
47 backoff: ExponentialBackoff,
48 recovery_error_decrease_factor: Option<u32>,
49 error_count: Arc<AtomicU32>,
50}
51
52impl<C> RollingExponentialBackoffRetryPolicy<C> {
53 pub fn new(classifier: C, backoff: ExponentialBackoff) -> Self {
57 Self {
58 classifier,
59 retry_lifecycle: DefaultDebugRetryLifecycle,
60 backoff,
61 recovery_error_decrease_factor: None,
62 error_count: Arc::new(AtomicU32::new(0)),
63 }
64 }
65}
66
67impl<C, L> RollingExponentialBackoffRetryPolicy<C, L> {
68 pub fn with_recovery_error_decrease_factor(mut self, factor: Option<u32>) -> Self {
75 self.recovery_error_decrease_factor = factor;
76 self
77 }
78
79 pub fn with_retry_lifecycle<L2>(self, retry_lifecycle: L2) -> RollingExponentialBackoffRetryPolicy<C, L2> {
85 RollingExponentialBackoffRetryPolicy {
86 classifier: self.classifier,
87 retry_lifecycle,
88 backoff: self.backoff,
89 recovery_error_decrease_factor: self.recovery_error_decrease_factor,
90 error_count: self.error_count,
91 }
92 }
93}
94
95impl<C, L, Req, Res, Error> Policy<Req, Res, Error> for RollingExponentialBackoffRetryPolicy<C, L>
96where
97 C: RetryClassifier<Res, Error>,
98 L: RetryLifecycle<Req, Res, Error>,
99 Req: Clone,
100{
101 type Future = Sleep;
102
103 fn retry(&mut self, request: &mut Req, response: &mut Result<Res, Error>) -> Option<Self::Future> {
104 if self.classifier.should_retry(response) {
105 let error_count = self.error_count.fetch_add(1, Relaxed) + 1;
107 let backoff_dur = self.backoff.get_backoff_duration(error_count);
108
109 self.retry_lifecycle
110 .before_retry(request, response, backoff_dur, error_count);
111
112 Some(sleep(backoff_dur))
113 } else {
114 self.retry_lifecycle.after_success(request, response);
115
116 match self.recovery_error_decrease_factor {
118 Some(factor) => {
119 debug!(decrease_factor = factor, "Decreasing error after successful response.");
120
121 let _ = self
124 .error_count
125 .fetch_update(AcqRel, Relaxed, |count| Some(count.saturating_sub(factor)));
126 }
127 None => {
128 debug!("Resetting error count to zero after successful response.");
129
130 self.error_count.store(0, Relaxed);
131 }
132 }
133
134 None
135 }
136 }
137
138 fn clone_request(&mut self, req: &Req) -> Option<Req> {
139 Some(req.clone())
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use std::{sync::atomic::Ordering::Relaxed, time::Duration};
146
147 use tower::retry::Policy;
148
149 use super::*;
150 use crate::net::util::retry::ExponentialBackoff;
151
152 struct ErrIsRetriable;
155
156 impl RetryClassifier<(), ()> for ErrIsRetriable {
157 fn should_retry(&self, response: &Result<(), ()>) -> bool {
158 response.is_err()
159 }
160 }
161
162 type TestPolicy = RollingExponentialBackoffRetryPolicy<ErrIsRetriable, DefaultDebugRetryLifecycle>;
163
164 fn test_policy(recovery_error_decrease_factor: Option<u32>) -> TestPolicy {
165 let backoff = ExponentialBackoff::new(Duration::from_millis(1), Duration::from_millis(100));
166 RollingExponentialBackoffRetryPolicy::new(ErrIsRetriable, backoff)
167 .with_recovery_error_decrease_factor(recovery_error_decrease_factor)
168 }
169
170 fn drive(policy: &mut TestPolicy, mut response: Result<(), ()>) -> bool {
172 Policy::<(), (), ()>::retry(policy, &mut (), &mut response).is_some()
173 }
174
175 #[tokio::test]
176 async fn recovery_decrease_factor_reduces_error_count_by_fixed_amount() {
177 let mut policy = test_policy(Some(3));
178
179 for expected in 1..=5u32 {
181 assert!(drive(&mut policy, Err(())), "a failure should request a retry");
182 assert_eq!(policy.error_count.load(Relaxed), expected);
183 }
184
185 assert!(!drive(&mut policy, Ok(())), "a success should not request a retry");
188 assert_eq!(policy.error_count.load(Relaxed), 2);
189
190 assert!(!drive(&mut policy, Ok(())));
192 assert_eq!(policy.error_count.load(Relaxed), 0);
193 }
194
195 #[tokio::test]
196 async fn no_recovery_factor_resets_error_count_to_zero_on_success() {
197 let mut policy = test_policy(None);
198
199 for _ in 0..4 {
200 assert!(drive(&mut policy, Err(())));
201 }
202 assert_eq!(policy.error_count.load(Relaxed), 4);
203
204 assert!(!drive(&mut policy, Ok(())));
206 assert_eq!(policy.error_count.load(Relaxed), 0);
207 }
208}