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