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