Skip to main content

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}