Skip to main content

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;