zebra_network/peer_set/candidate_set/rate_limit.rs
1//! Rate-limiting middleware for candidate peer selection and crawling.
2
3use std::{
4 sync::{Arc, Mutex},
5 task::{Context, Poll},
6 time::Duration,
7};
8
9use futures::{future, future::BoxFuture, FutureExt};
10use tokio::time::{sleep_until, Instant};
11use tower::Service;
12
13/// A middleware that limits how often the inner service is called.
14///
15/// While the rate limit applies, calls are skipped: they immediately return
16/// `S::Response::default()`, without calling the inner service or consuming
17/// any rate-limit budget.
18///
19/// The limit is claimed for as long as a call is running, so concurrent calls
20/// are skipped however long that call takes, and recharged when it finishes,
21/// so the interval runs from the end of one call to the start of the next.
22///
23/// Clones share one rate limit, so only clone this around clones of one inner
24/// service — around distinct services it would limit them as if they were one.
25#[derive(Clone, Debug)]
26pub struct RateLimitBySkipping<S> {
27 /// The rate-limited inner service.
28 inner: S,
29
30 /// The minimum interval between inner calls.
31 interval: Duration,
32
33 /// The next time the inner service is allowed to be called,
34 /// shared between clones of this middleware.
35 ///
36 /// `None` while a call is in flight, so calls are skipped until it
37 /// finishes, even if it runs for longer than `interval`.
38 next_allowed: Arc<Mutex<Option<Instant>>>,
39}
40
41impl<S> RateLimitBySkipping<S> {
42 /// Returns a new [`RateLimitBySkipping`] wrapping `inner`,
43 /// limiting calls to one per `interval`.
44 pub fn new(inner: S, interval: Duration) -> Self {
45 Self {
46 inner,
47 interval,
48 next_allowed: Arc::new(Mutex::new(Some(Instant::now()))),
49 }
50 }
51}
52
53impl<S, Request> Service<Request> for RateLimitBySkipping<S>
54where
55 S: Service<Request>,
56 S::Response: Default + Send + 'static,
57 S::Future: Send + 'static,
58 S::Error: Send + 'static,
59{
60 type Response = S::Response;
61 type Error = S::Error;
62 type Future = BoxFuture<'static, Result<S::Response, S::Error>>;
63
64 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
65 // Skipped calls reserve inner readiness without using it, which is
66 // fine here, because the inner crawl service is always ready.
67 self.inner.poll_ready(cx)
68 }
69
70 fn call(&mut self, request: Request) -> Self::Future {
71 {
72 let mut next_allowed = self
73 .next_allowed
74 .lock()
75 .expect("mutex should be unpoisoned");
76
77 // While rate-limited, or while another call is in flight, skip the
78 // call and return the default response immediately.
79 match *next_allowed {
80 None => return future::ready(Ok(S::Response::default())).boxed(),
81 Some(allowed_at) if Instant::now() < allowed_at => {
82 return future::ready(Ok(S::Response::default())).boxed()
83 }
84 Some(_) => {}
85 }
86
87 // Claim the rate limit for as long as the call runs, so concurrent
88 // calls are skipped even if it takes longer than `interval`.
89 *next_allowed = None;
90 }
91
92 let call = self.inner.call(request);
93 let next_allowed = self.next_allowed.clone();
94 let interval = self.interval;
95
96 async move {
97 let result = call.await;
98
99 // Recharge the rate limit from the completion time, so the
100 // interval runs from the end of this call.
101 //
102 // # Correctness
103 //
104 // This must also happen when the call fails: the claim above is
105 // an open-ended `None`, so leaving it in place after an error
106 // would skip every later call for the lifetime of the process.
107 *next_allowed.lock().expect("mutex should be unpoisoned") =
108 Some(Instant::now() + interval);
109
110 result
111 }
112 .boxed()
113 }
114}
115
116/// A middleware that rate-limits the responses that yield a result,
117/// as decided by the `charges` predicate.
118///
119/// Every call is forwarded to the inner service immediately. If `charges`
120/// returns `true` for the response, this middleware reserves the earliest
121/// free pacing slot, waits until that slot's time, and only then returns the
122/// response. Responses that don't charge are returned immediately, without
123/// consuming any rate-limit budget.
124///
125/// Because slots are reserved atomically, concurrent yielding responses are
126/// returned at least `interval` apart, in slot reservation order.
127#[derive(Clone, Debug)]
128pub struct RateLimitOnYield<S, Response> {
129 /// The inner service.
130 inner: S,
131
132 /// The minimum interval between yielding responses.
133 interval: Duration,
134
135 /// Returns `true` if a response yields a result,
136 /// consuming rate-limit budget.
137 charges: fn(&Response) -> bool,
138
139 /// The next free pacing slot, shared between clones of this middleware.
140 next_allowed: Arc<Mutex<Instant>>,
141}
142
143impl<S, Response> RateLimitOnYield<S, Response> {
144 /// Returns a new [`RateLimitOnYield`] wrapping `inner`, limiting the
145 /// responses that `charges` to one per `interval`.
146 pub fn new(inner: S, interval: Duration, charges: fn(&Response) -> bool) -> Self {
147 Self {
148 inner,
149 interval,
150 charges,
151 next_allowed: Arc::new(Mutex::new(Instant::now())),
152 }
153 }
154}
155
156impl<S, Request, Response> Service<Request> for RateLimitOnYield<S, Response>
157where
158 S: Service<Request, Response = Response>,
159 S::Future: Send + 'static,
160 S::Error: Send + 'static,
161 Response: Send + 'static,
162{
163 type Response = Response;
164 type Error = S::Error;
165 type Future = BoxFuture<'static, Result<Response, S::Error>>;
166
167 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
168 self.inner.poll_ready(cx)
169 }
170
171 fn call(&mut self, request: Request) -> Self::Future {
172 let response = self.inner.call(request);
173 let charges = self.charges;
174 let interval = self.interval;
175 let next_allowed = self.next_allowed.clone();
176
177 async move {
178 let response = response.await?;
179
180 if charges(&response) {
181 // Reserve the earliest free pacing slot, then sleep until the
182 // slot's time. Reserving before sleeping means concurrent
183 // yields wake at least `interval` apart.
184 let slot = {
185 let mut next_allowed = next_allowed.lock().expect("mutex should be unpoisoned");
186 let now = Instant::now();
187 let slot = if *next_allowed > now {
188 *next_allowed
189 } else {
190 now
191 };
192 *next_allowed = slot + interval;
193 slot
194 };
195
196 sleep_until(slot).await;
197
198 // Recharge from the actual wake time, like the replaced
199 // manual timer, so scheduler wake-up latency doesn't
200 // accumulate as a pacing deficit. Only ever extend the shared
201 // time, so concurrent slot reservations are never undone.
202 {
203 let mut next_allowed = next_allowed.lock().expect("mutex should be unpoisoned");
204 let recharge = Instant::now() + interval;
205 if recharge > *next_allowed {
206 *next_allowed = recharge;
207 }
208 }
209 }
210
211 Ok(response)
212 }
213 .boxed()
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use std::sync::atomic::{AtomicUsize, Ordering};
220
221 use tower::{service_fn, ServiceExt};
222
223 use super::*;
224
225 /// The interval used by the tests, long enough that it never elapses
226 /// on its own during a test run.
227 const TEST_INTERVAL: Duration = Duration::from_secs(30);
228
229 /// Test that a call running for longer than the interval still skips
230 /// concurrent calls, rather than letting a second call through once the
231 /// interval has elapsed.
232 #[tokio::test(start_paused = true)]
233 async fn long_call_skips_concurrent_calls() {
234 let _init_guard = zebra_test::init();
235
236 let calls = Arc::new(AtomicUsize::new(0));
237
238 let inner = {
239 let calls = calls.clone();
240 service_fn(move |()| {
241 let calls = calls.clone();
242 async move {
243 calls.fetch_add(1, Ordering::SeqCst);
244 // Run for much longer than the rate-limit interval.
245 tokio::time::sleep(TEST_INTERVAL * 4).await;
246 Ok::<bool, std::convert::Infallible>(true)
247 }
248 })
249 };
250
251 let service = RateLimitBySkipping::new(inner, TEST_INTERVAL);
252
253 // Start a long call, and let it reach the inner service.
254 let long_call = tokio::spawn(service.clone().oneshot(()));
255 tokio::task::yield_now().await;
256
257 // Wait past the interval, while the first call is still running.
258 tokio::time::sleep(TEST_INTERVAL * 2).await;
259
260 let skipped = service
261 .clone()
262 .oneshot(())
263 .await
264 .expect("skipped calls return the default response");
265
266 assert!(
267 !skipped,
268 "a call made while another is in flight must be skipped, \
269 even after the interval has elapsed",
270 );
271 assert_eq!(
272 calls.load(Ordering::SeqCst),
273 1,
274 "the inner service must only be called once while a call is in flight",
275 );
276
277 assert!(long_call.await.expect("call task should not panic").is_ok());
278 }
279
280 /// Test that a failed call recharges the rate limit, instead of leaving
281 /// the open-ended in-flight claim in place forever.
282 #[tokio::test(start_paused = true)]
283 async fn failed_call_recharges_the_rate_limit() {
284 let _init_guard = zebra_test::init();
285
286 let service = RateLimitBySkipping::new(
287 service_fn(|()| async { Err::<bool, &'static str>("inner service error") }),
288 TEST_INTERVAL,
289 );
290
291 service
292 .clone()
293 .oneshot(())
294 .await
295 .expect_err("the inner error should be returned");
296
297 // After the interval, calls are allowed again.
298 tokio::time::sleep(TEST_INTERVAL * 2).await;
299
300 service
301 .clone()
302 .oneshot(())
303 .await
304 .expect_err("a later call should reach the inner service, not be skipped forever");
305 }
306}