Skip to main content

libdd_common/
rate_limiter.rs

1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4use core::sync::atomic::{AtomicU64, Ordering};
5use core::time::Duration;
6
7/// A rate limiter whose complete mutable state is one atomic deadline.
8///
9/// Granularity is supplied by the caller and is not part of the limiter state.
10#[repr(transparent)]
11#[derive(Default)]
12pub struct LocalLimiter {
13    deadline: AtomicU64,
14}
15
16const TIME_PER_SECOND: u64 = 1_000_000_000; // nanoseconds
17
18/// When set to a non-zero value, `now()` returns this instead of the real clock.
19/// This allows tests to control time deterministically, avoiding flakiness from
20/// wall-clock timing on CI machines.
21#[cfg(test)]
22static MOCK_NOW: AtomicU64 = AtomicU64::new(0);
23
24/// Monotonic nanoseconds from a system-wide clock, comparable across processes.
25pub fn now() -> u64 {
26    monotonic_now().unwrap_or(0)
27}
28
29fn monotonic_now() -> Option<u64> {
30    #[cfg(test)]
31    {
32        let mock = MOCK_NOW.load(Ordering::Relaxed);
33        if mock != 0 {
34            return Some(mock);
35        }
36    }
37    #[cfg(windows)]
38    let now = {
39        use windows_sys::Win32::System::Performance::{
40            QueryPerformanceCounter, QueryPerformanceFrequency,
41        };
42
43        static FREQUENCY: AtomicU64 = AtomicU64::new(0);
44
45        let mut frequency = FREQUENCY.load(Ordering::Relaxed);
46        if frequency == 0 {
47            let mut measured_frequency = 0;
48            // SAFETY: the output pointer refers to valid writable storage.
49            if unsafe { QueryPerformanceFrequency(&mut measured_frequency) } == 0 {
50                return None;
51            }
52            frequency = u64::try_from(measured_frequency)
53                .ok()
54                .filter(|frequency| *frequency != 0)?;
55            FREQUENCY.store(frequency, Ordering::Relaxed);
56        }
57
58        let mut ticks = 0;
59        // SAFETY: the output pointer refers to valid writable storage.
60        if unsafe { QueryPerformanceCounter(&mut ticks) } == 0 {
61            return None;
62        }
63        let nanos = u128::try_from(ticks)
64            .ok()?
65            .checked_mul(u128::from(TIME_PER_SECOND))?
66            .checked_div(u128::from(frequency))?;
67        u64::try_from(nanos).ok()?
68    };
69    #[cfg(not(windows))]
70    let now = {
71        let mut ts: libc::timespec = libc::timespec {
72            tv_sec: 0,
73            tv_nsec: 0,
74        };
75        // SAFETY: ts points to valid writable storage.
76        if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) } != 0 {
77            return None;
78        }
79        u64::try_from(ts.tv_sec)
80            .ok()?
81            .checked_mul(TIME_PER_SECOND)?
82            .checked_add(u64::try_from(ts.tv_nsec).ok()?)?
83    };
84    Some(now)
85}
86
87impl LocalLimiter {
88    /// Clear all accumulated virtual-time debt.
89    pub fn reset(&self) {
90        self.deadline.store(0, Ordering::Relaxed);
91    }
92
93    /// Try to consume one of `limit` admissions per `granularity`.
94    pub fn inc(&self, limit: u32, granularity: Duration) -> bool {
95        let Some(granularity) = duration_nanos(granularity) else {
96            return false;
97        };
98        monotonic_now().is_some_and(|now| admit(&self.deadline, granularity, limit, now))
99    }
100
101    /// Return the outstanding virtual-time debt as a fraction of `granularity`.
102    pub fn rate(&self, granularity: Duration) -> f64 {
103        let Some(granularity) = duration_nanos(granularity) else {
104            return 1.0;
105        };
106        monotonic_now().map_or(1.0, |now| {
107            debt(&self.deadline, granularity, now).clamp(0.0, 1.0)
108        })
109    }
110
111    /// Return whether the limiter still has outstanding virtual-time debt.
112    pub fn is_active(&self) -> bool {
113        monotonic_now().is_none_or(|now| self.deadline.load(Ordering::Relaxed) > now)
114    }
115}
116
117fn duration_nanos(duration: Duration) -> Option<u64> {
118    let nanos = u64::try_from(duration.as_nanos()).ok()?;
119    (nanos > 0).then_some(nanos)
120}
121
122/// Atomically add one admission's virtual-time debt.
123///
124/// Each admission costs `ceil(interval_ns / limit)` nanoseconds. Idle time clears debt, and
125/// the limiter accepts at most `limit` outstanding costs. The capacity uses the rounded cost,
126/// rather than `interval_ns`, so rounding still permits the full initial burst.
127fn admit(deadline: &AtomicU64, interval_ns: u64, limit: u32, now: u64) -> bool {
128    if limit == 0 {
129        return false;
130    }
131    let admissions = u64::from(limit);
132    let cost_ns = interval_ns.div_ceil(admissions);
133    let Some(capacity_ns) = admissions.checked_mul(cost_ns) else {
134        return false;
135    };
136    let Some(max_deadline) = now.checked_add(capacity_ns) else {
137        return false;
138    };
139    // `try_update` is the new name, but is not available on the workspace MSRV.
140    #[allow(deprecated)]
141    let update = deadline.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |previous_deadline| {
142        let next_deadline = previous_deadline.max(now).checked_add(cost_ns)?;
143        (next_deadline <= max_deadline).then_some(next_deadline)
144    });
145    update.is_ok()
146}
147
148fn debt(deadline: &AtomicU64, granularity: u64, now: u64) -> f64 {
149    // Floating-point precision is sufficient for this diagnostic ratio.
150    deadline.load(Ordering::Relaxed).saturating_sub(now) as f64 / granularity as f64
151}
152
153#[cfg(test)]
154mod tests {
155    use crate::rate_limiter::{
156        LocalLimiter, MOCK_NOW, TIME_PER_SECOND, admit, debt, duration_nanos, now,
157    };
158    use core::sync::atomic::{AtomicU64, Ordering};
159    use core::time::Duration;
160
161    fn set_mock_time(nanos: u64) {
162        MOCK_NOW.store(nanos, Ordering::Relaxed);
163    }
164
165    fn advance_mock_time(nanos: u64) {
166        MOCK_NOW.fetch_add(nanos, Ordering::Relaxed);
167    }
168
169    #[test]
170    #[cfg_attr(miri, ignore)]
171    fn test_rate_limiter() {
172        set_mock_time(TIME_PER_SECOND);
173        let limiter = LocalLimiter::default();
174        let granularity = Duration::from_secs(1);
175
176        assert!(limiter.inc(2, granularity));
177        assert_eq!(limiter.rate(granularity), 0.5);
178        assert!(limiter.inc(2, granularity));
179        assert_eq!(limiter.rate(granularity), 1.0);
180        assert!(!limiter.inc(2, granularity));
181
182        advance_mock_time(TIME_PER_SECOND / 2);
183        assert_eq!(limiter.rate(granularity), 0.5);
184        assert!(limiter.inc(2, granularity));
185        assert_eq!(limiter.rate(granularity), 1.0);
186
187        advance_mock_time(TIME_PER_SECOND);
188        assert_eq!(limiter.rate(granularity), 0.0);
189        assert!(!limiter.inc(0, granularity));
190        assert!(!limiter.inc(1, Duration::ZERO));
191
192        set_mock_time(0);
193    }
194
195    #[test]
196    #[cfg_attr(miri, ignore)]
197    fn test_now_monotonic() {
198        let t1 = now();
199        assert!(t1 > 0);
200        let t2 = now();
201        assert!(t2 >= t1);
202    }
203
204    #[test]
205    fn test_local_limiter_algorithm() {
206        let deadline = AtomicU64::new(0);
207        assert!(!admit(&deadline, 100, 0, 1000));
208        for _ in 0..3 {
209            assert!(admit(&deadline, 100, 3, 1000));
210        }
211        assert!(!admit(&deadline, 100, 3, 1000));
212        assert_eq!(deadline.load(Ordering::Relaxed), 1102);
213        assert_eq!(debt(&deadline, 100, 1000), 1.02);
214        assert!(!admit(&deadline, 100, 3, 1033));
215        assert!(admit(&deadline, 100, 3, 1034));
216
217        assert_eq!(duration_nanos(Duration::ZERO), None);
218        assert_eq!(duration_nanos(Duration::from_secs(1)), Some(1_000_000_000));
219
220        let limiter = LocalLimiter::default();
221        assert_eq!(size_of::<LocalLimiter>(), size_of::<AtomicU64>());
222        limiter.deadline.store(42, Ordering::Relaxed);
223        limiter.reset();
224        assert_eq!(limiter.deadline.load(Ordering::Relaxed), 0);
225    }
226}