Skip to main content

rate_net/
lib.rs

1//! # rate-net
2//!
3//! A powerful, lock-free rate limiter for Rust. It answers one question as fast
4//! as the hardware allows — *"is this key allowed right now?"* — and answers it
5//! with a [`Decision`](#) (allow / deny plus a `retry-after`), across multiple
6//! algorithms, while tracking per-key state under high contention. Per-key
7//! state lives in a sharded concurrent map, so unrelated keys never contend and
8//! throughput scales with core count; each key's bucket is lock-free and memory
9//! is bounded by eviction, so a flood of unique keys hits a cap instead of
10//! growing without limit.
11//!
12//! rate-net does not reimplement token-bucket accounting. It consumes
13//! [`better-bucket`](https://crates.io/crates/better-bucket) for that and reads
14//! time from [`clock-lib`](https://crates.io/crates/clock-lib), then adds the
15//! per-key, multi-algorithm, retry-after machinery around them. It is the
16//! anchor crate of the `-net` domain group and is consumed by gatekeepers (such
17//! as `bouncer-io`) through the clean decision API — they call `check` and never
18//! reach into its internal state.
19//!
20//! ## Status
21//!
22//! Pre-1.0 **beta** (`0.9.0`): the **API is frozen** (since `0.7.0`) and locked
23//! in at the *type* level too — every public type is `Send + Sync + 'static`
24//! and that is asserted at compile time. The integration shake-out at `0.8.0`
25//! confirmed the surface is consumable through the public allow/deny API only,
26//! and `0.9.0` widens the concurrency coverage: a single hot key under eight
27//! threads against **every** algorithm admits exactly its quota — never more
28//! (no over-admit, no torn updates) and never fewer (no lost decrements, no
29//! deadlock). The five algorithms sit behind the one [`Limiter`] trait (token
30//! bucket by default; the leaky bucket and window algorithms under the
31//! `algorithms` feature), with the Tier-2 [`Builder`], an optional `AsyncLimiter`
32//! await-until-ready layer (`async` feature), runnable
33//! [examples](https://github.com/jamesgober/rate-net/tree/main/examples), and a
34//! `criterion` suite. Per-key state lives in a purpose-built **sharded store**
35//! (an existing-key [`check`](RateLimiter::check) takes only a shard read lock
36//! plus the algorithm's atomic accounting, so unrelated keys never contend),
37//! memory is **bounded by eviction**, and the steady-state check is
38//! **allocation-free**. From here, the only changes before `1.0` are bug fixes
39//! and documentation polish.
40//!
41//! ```
42//! # #[cfg(feature = "std")] {
43//! use rate_net::{RateLimiter, Decision};
44//!
45//! // 100 requests per second, per key.
46//! let limiter = RateLimiter::per_second(100);
47//!
48//! match limiter.check("user:42") {
49//!     Decision::Allow => {
50//!         // allowed — serve the request
51//!     }
52//!     Decision::Deny { retry_after } => {
53//!         // denied — return 429 with `Retry-After: retry_after`
54//!         let _ = retry_after;
55//!     }
56//!     _ => {}
57//! }
58//! # }
59//! ```
60//!
61//! ## Design goals
62//!
63//! - **Lock-free per key.** Each key's bucket delegates to `better-bucket`'s
64//!   atomic compare-and-swap core; no lock sits on the check path.
65//! - **Sharded state.** Per-key state lives in a sharded concurrent map, so
66//!   unrelated keys land in different shards and never serialize on each other.
67//!   Throughput scales with cores; shard count is tunable.
68//! - **Zero-allocation steady state.** A `check` on an existing key allocates
69//!   nothing; allocation happens only the first time a key is seen.
70//! - **Bounded memory.** Idle keys are evicted (LRU/TTL) on an amortized,
71//!   incremental schedule that never stops the world on the hot path. A hostile
72//!   unique-key flood reaches the eviction cap and stays there.
73//! - **Never over-admits.** For any key and window, admitted requests never
74//!   exceed the configured quota under any concurrent interleaving — proven per
75//!   algorithm with `loom` and `proptest`.
76//! - **Lazy, runtime-free.** Refill and expiry are computed from a monotonic
77//!   clock on access; there is no background timer thread and the core has no
78//!   async-runtime dependency.
79//!
80//! ## Feature flags
81//!
82//! | Feature | Default | Description |
83//! |---------|---------|-------------|
84//! | `std`        | yes | Standard library. Required for the sharded per-key store and eviction. With it off the crate is `no_std`; the scaffold then exposes only [`VERSION`], and the pared-down single-key mode follows in a later release. |
85//! | `algorithms` | no  | The full algorithm suite beyond the default token bucket: leaky bucket, fixed window, sliding-window log, sliding-window counter. |
86//! | `async`      | no  | Optional async-friendly wrapper layer. Additive only — the core has no runtime dependency. |
87
88// `no_std` for the library build when `std` is off, but always link `std` under
89// `test` so the unit-test harness (and dev-dependencies) have what they need.
90#![cfg_attr(all(not(feature = "std"), not(test)), no_std)]
91#![deny(warnings)]
92#![deny(missing_docs)]
93#![deny(unsafe_op_in_unsafe_fn)]
94#![deny(unused_must_use)]
95#![deny(unused_results)]
96#![deny(clippy::unwrap_used)]
97#![deny(clippy::expect_used)]
98#![deny(clippy::todo)]
99#![deny(clippy::unimplemented)]
100#![deny(clippy::print_stdout)]
101#![deny(clippy::print_stderr)]
102#![deny(clippy::dbg_macro)]
103#![deny(clippy::unreachable)]
104#![deny(clippy::undocumented_unsafe_blocks)]
105
106// The limiter surface requires the standard library (the concurrent per-key
107// store, the clock-driven token bucket, and the domain error type). With `std`
108// off the crate is no_std and exposes only `VERSION`.
109#[cfg(feature = "std")]
110mod algo;
111#[cfg(feature = "std")]
112mod algorithm;
113#[cfg(feature = "async")]
114mod async_limiter;
115#[cfg(feature = "std")]
116mod builder;
117#[cfg(feature = "std")]
118mod decision;
119#[cfg(feature = "std")]
120mod error;
121#[cfg(feature = "std")]
122mod eviction;
123#[cfg(feature = "std")]
124mod key;
125#[cfg(feature = "std")]
126mod limiter;
127#[cfg(feature = "std")]
128mod quota;
129#[cfg(feature = "std")]
130mod store;
131
132#[cfg(feature = "std")]
133pub use crate::algorithm::Algorithm;
134#[cfg(feature = "async")]
135pub use crate::async_limiter::AsyncLimiter;
136#[cfg(feature = "std")]
137pub use crate::builder::Builder;
138#[cfg(feature = "std")]
139pub use crate::decision::Decision;
140#[cfg(feature = "std")]
141pub use crate::error::RateLimiterError;
142#[cfg(feature = "std")]
143pub use crate::eviction::{DEFAULT_MAX_KEYS, Eviction};
144#[cfg(feature = "std")]
145pub use crate::key::Key;
146#[cfg(feature = "std")]
147pub use crate::limiter::{Limiter, RateLimiter};
148#[cfg(feature = "std")]
149pub use crate::quota::Quota;
150
151/// The version of this crate, taken from `Cargo.toml` at compile time.
152///
153/// Exposed so a consumer can report the exact `rate-net` build it links
154/// against — useful in diagnostics and version-skew checks across a dependency
155/// tree.
156///
157/// # Examples
158///
159/// ```
160/// // Reports the current 0.x series and carries a major.minor.patch core.
161/// let version = rate_net::VERSION;
162/// assert!(version.starts_with("0."));
163/// assert_eq!(version.split('.').count(), 3);
164/// ```
165pub const VERSION: &str = env!("CARGO_PKG_VERSION");
166
167#[cfg(test)]
168mod tests {
169    use super::VERSION;
170
171    #[test]
172    fn test_version_is_well_formed_semver() {
173        // A `major.minor.patch` core with no empty components.
174        let parts: Vec<&str> = VERSION.split('.').collect();
175        assert_eq!(parts.len(), 3, "expected major.minor.patch, got {VERSION}");
176        assert!(parts.iter().all(|part| !part.is_empty()));
177    }
178}