libdd_common/
rate_limiter.rs1use core::sync::atomic::{AtomicU64, Ordering};
5use core::time::Duration;
6
7#[repr(transparent)]
11#[derive(Default)]
12pub struct LocalLimiter {
13 deadline: AtomicU64,
14}
15
16const TIME_PER_SECOND: u64 = 1_000_000_000; #[cfg(test)]
22static MOCK_NOW: AtomicU64 = AtomicU64::new(0);
23
24pub 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 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 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 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 pub fn reset(&self) {
90 self.deadline.store(0, Ordering::Relaxed);
91 }
92
93 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 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 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
122fn 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 #[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 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}