subms_rate_limiter/lib.rs
1//! Lock-free rate limiter using the GCRA (Generic Cell Rate Algorithm) formulation.
2//!
3//! State is a single `AtomicU64` holding `tat_ns` - the theoretical arrival
4//! time of the next slot. `try_acquire` reads `tat`, computes the new value
5//! (`max(now, tat) + period`), and CAS-loops it in. Rejects when the new
6//! `tat` would land more than `burst_ns` in the future.
7//!
8//! ```
9//! use subms_rate_limiter::RateLimiter;
10//!
11//! // 1000 permits/sec, allow bursts of 10.
12//! let rl = RateLimiter::new(1000.0, 10);
13//! assert!(rl.try_acquire());
14//! ```
15//!
16//! Thread-safety: [`RateLimiter`] is `Send + Sync` and every method takes
17//! `&self`. Share one instance across threads behind an `Arc`; there is no
18//! interior lock and no `&mut self` path.
19//!
20//! Full writeup, design notes and measured benchmarks:
21//! <https://www.submillisecond.com/cookbook/recipes/subms-rate-limiter>
22
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::time::{Duration, Instant};
25
26/// Outcome of [`RateLimiter::try_acquire_with_retry`]: a permit was granted, or
27/// the caller should wait at least `Retry(d)` before a retry will conform - the
28/// value for an HTTP `Retry-After`. Under contention the duration is a
29/// best-effort hint (another thread may take the slot first), the guarantee
30/// every lock-free rate limiter's retry-after carries.
31///
32/// `Unattainable` is the typed answer to a request no amount of waiting can
33/// satisfy: `n` above `burst_capacity` overshoots the burst window even from a
34/// fully idle limiter, so it is a sizing error rather than backpressure.
35/// `governor` reports the same condition as `InsufficientCapacity`.
36#[derive(Copy, Clone, Debug, PartialEq, Eq)]
37pub enum Acquire {
38 Ok,
39 Retry(Duration),
40 Unattainable { burst_capacity: u64 },
41}
42
43/// Lock-free token-bucket / GCRA rate limiter.
44pub struct RateLimiter {
45 /// Theoretical arrival time of the next permit, in ns since the limiter
46 /// was created.
47 tat_ns: AtomicU64,
48 /// ns per permit at the target rate. `1_000_000_000 / rate_per_sec`.
49 period_ns: u64,
50 /// Max burst ahead of now, in ns. `capacity * period_ns`.
51 burst_ns: u64,
52 /// Monotonic clock origin. All timestamps stored relative to this.
53 origin: Instant,
54}
55
56impl RateLimiter {
57 /// `rate_per_sec` permits per second, sustained. `burst_capacity` permits
58 /// may be drawn in a burst before throttling kicks in. A capacity of 0 is
59 /// floored to 1 - a window of zero rejects every request including the
60 /// first, which is a broken limiter rather than a strict one.
61 pub fn new(rate_per_sec: f64, burst_capacity: u64) -> Self {
62 let period_ns = (1_000_000_000.0 / rate_per_sec) as u64;
63 let burst_ns = period_ns.saturating_mul(burst_capacity.max(1));
64 Self {
65 tat_ns: AtomicU64::new(0),
66 period_ns,
67 burst_ns,
68 origin: Instant::now(),
69 }
70 }
71
72 /// Try to acquire one permit. Returns `true` if granted, `false` if the
73 /// caller should be rejected (rate exceeded). Wait-free uncontended;
74 /// CAS-loop under contention.
75 pub fn try_acquire(&self) -> bool {
76 self.try_acquire_at(self.now_ns())
77 }
78
79 /// `try_acquire` against a caller-supplied `now` (ns since [`Self::now_ns`]'s
80 /// origin) instead of the internal monotonic clock. This is the driven-time
81 /// entry point: a simulation, a replay harness or a deterministic test steps
82 /// `now` itself rather than sleeping on the wall clock. `governor` exposes
83 /// the same idea as `check_at`.
84 pub fn try_acquire_at(&self, now: u64) -> bool {
85 loop {
86 let tat = self.tat_ns.load(Ordering::Acquire);
87 // New TAT = max(now, tat) + period. Permit is allowed iff the new
88 // TAT lands within `burst_ns` of `now`.
89 let new_tat = tat.max(now).saturating_add(self.period_ns);
90 if new_tat.saturating_sub(now) > self.burst_ns {
91 return false;
92 }
93 // Race other producers for the slot.
94 match self.tat_ns.compare_exchange_weak(
95 tat,
96 new_tat,
97 Ordering::AcqRel,
98 Ordering::Acquire,
99 ) {
100 Ok(_) => return true,
101 Err(_) => continue,
102 }
103 }
104 }
105
106 /// Like [`Self::try_acquire`], but on rejection reports how long to wait
107 /// before a retry will conform - the value for an HTTP `Retry-After`. A
108 /// grant advances the limiter exactly as `try_acquire` does; a rejection
109 /// leaves it untouched.
110 pub fn try_acquire_with_retry(&self) -> Acquire {
111 self.try_acquire_with_retry_at(self.now_ns())
112 }
113
114 /// `try_acquire_with_retry` against a caller-supplied `now`.
115 pub fn try_acquire_with_retry_at(&self, now: u64) -> Acquire {
116 self.try_acquire_n_with_retry_at(now, 1)
117 }
118
119 /// Draw `n` permits at once - a weighted request, where a heavy message
120 /// costs more of the budget than a light one. All-or-nothing: a rejected
121 /// call spends nothing. `n` above [`Self::burst_capacity`] can never be
122 /// granted; use [`Self::try_acquire_n_with_retry`] to see that as a typed
123 /// outcome instead of a bare `false`.
124 pub fn try_acquire_n(&self, n: u64) -> bool {
125 self.try_acquire_n_at(self.now_ns(), n)
126 }
127
128 /// [`Self::try_acquire_n`] against a caller-supplied `now`.
129 pub fn try_acquire_n_at(&self, now: u64, n: u64) -> bool {
130 matches!(self.try_acquire_n_with_retry_at(now, n), Acquire::Ok)
131 }
132
133 /// [`Self::try_acquire_n`] reporting the retry-after on rejection.
134 pub fn try_acquire_n_with_retry(&self, n: u64) -> Acquire {
135 self.try_acquire_n_with_retry_at(self.now_ns(), n)
136 }
137
138 /// The weighted GCRA step: one request of weight `n` costs `n` periods of
139 /// theoretical arrival time. `n = 0` is a free probe that neither advances
140 /// the limiter nor can be rejected.
141 pub fn try_acquire_n_with_retry_at(&self, now: u64, n: u64) -> Acquire {
142 if n == 0 {
143 return Acquire::Ok;
144 }
145 let cost = self.period_ns.saturating_mul(n);
146 if cost > self.burst_ns {
147 return Acquire::Unattainable {
148 burst_capacity: self.burst_capacity(),
149 };
150 }
151 loop {
152 let tat = self.tat_ns.load(Ordering::Acquire);
153 let new_tat = tat.max(now).saturating_add(cost);
154 if new_tat.saturating_sub(now) > self.burst_ns {
155 // Rejected: wait until the slot re-enters the burst window.
156 let wait = new_tat.saturating_sub(self.burst_ns).saturating_sub(now);
157 return Acquire::Retry(Duration::from_nanos(wait));
158 }
159 match self.tat_ns.compare_exchange_weak(
160 tat,
161 new_tat,
162 Ordering::AcqRel,
163 Ordering::Acquire,
164 ) {
165 Ok(_) => return Acquire::Ok,
166 Err(_) => continue,
167 }
168 }
169 }
170
171 /// How long until `n` permits would conform, without taking them.
172 /// `Some(ZERO)` means a call right now would be granted; `None` means `n`
173 /// exceeds the burst capacity and no wait will help. Read-only: unlike
174 /// `try_acquire`, this never advances the limiter, so a scheduler can plan
175 /// against it without spending budget.
176 pub fn time_until_ready(&self, n: u64) -> Option<Duration> {
177 self.time_until_ready_at(self.now_ns(), n)
178 }
179
180 /// [`Self::time_until_ready`] against a caller-supplied `now`.
181 pub fn time_until_ready_at(&self, now: u64, n: u64) -> Option<Duration> {
182 if n == 0 {
183 return Some(Duration::ZERO);
184 }
185 let cost = self.period_ns.saturating_mul(n);
186 if cost > self.burst_ns {
187 return None;
188 }
189 let tat = self.tat_ns.load(Ordering::Acquire);
190 let new_tat = tat.max(now).saturating_add(cost);
191 if new_tat.saturating_sub(now) > self.burst_ns {
192 let wait = new_tat.saturating_sub(self.burst_ns).saturating_sub(now);
193 Some(Duration::from_nanos(wait))
194 } else {
195 Some(Duration::ZERO)
196 }
197 }
198
199 /// Block until `n` permits are granted or `timeout` elapses, whichever
200 /// comes first. Returns `false` without sleeping when the wait provably
201 /// exceeds the timeout, matching Guava's `tryAcquire(permits, timeout)`.
202 ///
203 /// Waiters are not queued, so this is not FIFO: several blocked callers
204 /// wake and race for the same slot. It sleeps by design and is outside the
205 /// per-op sub-ms claim.
206 pub fn acquire_within(&self, n: u64, timeout: Duration) -> bool {
207 let deadline = Instant::now() + timeout;
208 loop {
209 match self.try_acquire_n_with_retry(n) {
210 Acquire::Ok => return true,
211 Acquire::Unattainable { .. } => return false,
212 Acquire::Retry(wait) => {
213 let remaining = deadline.saturating_duration_since(Instant::now());
214 if wait > remaining {
215 return false;
216 }
217 std::thread::sleep(wait);
218 }
219 }
220 }
221 }
222
223 /// Drop all accumulated throttle state: the next `burst_capacity` permits
224 /// are granted immediately. For a session that reconnects and gets a fresh
225 /// allowance from the venue, or a test that reuses one limiter.
226 pub fn reset(&self) {
227 self.tat_ns.store(0, Ordering::Release);
228 }
229
230 /// Nanoseconds elapsed on the limiter's own monotonic clock. The value the
231 /// `_at` methods expect, so a caller can read the clock once and reuse it
232 /// across several limiters.
233 pub fn now_ns(&self) -> u64 {
234 self.origin.elapsed().as_nanos() as u64
235 }
236
237 /// Configured permits per second.
238 pub fn rate_per_sec(&self) -> f64 {
239 1_000_000_000.0 / self.period_ns as f64
240 }
241
242 /// Configured burst capacity (in permits).
243 pub fn burst_capacity(&self) -> u64 {
244 self.burst_ns.checked_div(self.period_ns).unwrap_or(0)
245 }
246}
247
248#[cfg(feature = "harness")]
249pub mod recipe;
250
251// Opt-in feature catalog. Each module is gated on its own Cargo
252// feature; the base GCRA limiter stays zero-dep + std-only.
253#[cfg(any(
254 feature = "token-bucket",
255 feature = "hierarchical",
256 feature = "distributed-backend",
257 feature = "metrics",
258 feature = "keyed",
259))]
260pub mod features;
261
262#[cfg(any(
263 feature = "token-bucket",
264 feature = "hierarchical",
265 feature = "distributed-backend",
266 feature = "metrics",
267 feature = "keyed",
268))]
269pub use features::clock::{Clock, SystemClock, TestClock};
270
271#[cfg(feature = "distributed-backend")]
272pub use features::distributed_backend::{Backend, DistributedLimiter, InMemoryBackend};
273#[cfg(feature = "hierarchical")]
274pub use features::hierarchical::HierarchicalLimiter;
275#[cfg(feature = "keyed")]
276pub use features::keyed::KeyedRateLimiter;
277#[cfg(feature = "metrics")]
278pub use features::metrics::{MeteredTokenBucket, MetricsSnapshot};
279#[cfg(feature = "token-bucket")]
280pub use features::token_bucket::TokenBucket;
281
282#[cfg(test)]
283#[path = "rate_limiter_tests.rs"]
284mod rate_limiter_tests;
285
286#[cfg(test)]
287#[path = "sample_app_tests.rs"]
288mod sample_app_tests;