Skip to main content

better_bucket/
lib.rs

1//! # better-bucket
2//!
3//! A genuinely better token bucket for Rust. The hot path — `try_acquire` — is
4//! designed to be **lock-free**, **allocation-free**, and **cache-aligned**: a
5//! single compare-and-swap over a packed `(tokens, last_refill_tick)` word.
6//! Refill is **lazy**, computed from a monotonic clock the instant you ask, so
7//! an idle bucket costs nothing — no background timer thread, no per-tick
8//! wakeups. The defining correctness property is that the bucket **never
9//! over-grants**: across any concurrent interleaving, the total tokens handed
10//! out never exceed capacity plus accrued refill.
11//!
12//! The crate is a single-purpose primitive. It owns token-bucket accounting and
13//! nothing else, so it can sit at the bottom of a dependency tree (its first
14//! consumer is the `rate-net` rate limiter) without dragging in an async
15//! runtime or a keyed-state store.
16//!
17//! ## Guarantees
18//!
19//! The safety contract holds under any concurrent interleaving, adversarial
20//! input, and extreme uptime: **no panic, no wrap, no over-grant, and tokens
21//! always within `[0, capacity]`.** It is defended by `loom` model checking, a
22//! multi-thread stress test, an allocation audit, an adversarial/edge suite, and
23//! `proptest`. `try_acquire` is a single `compare_exchange_weak` on a packed
24//! atomic word — allocation-free, cache-line aligned, with a division-free
25//! fixed-point refill. The bucket's own accounting measures a few nanoseconds;
26//! end-to-end `try_acquire` is bounded by the monotonic clock read (see
27//! `docs/BENCHMARKS.md`).
28//!
29//! Token bucket is the crate's sole algorithm by design — leaky-bucket and
30//! sliding-window limiting live in the downstream `rate-net` crate.
31//!
32//! ```
33//! # #[cfg(feature = "clock")] {
34//! use better_bucket::Bucket;
35//!
36//! // 100 tokens per second, capacity 100.
37//! let bucket = Bucket::per_second(100);
38//!
39//! if bucket.try_acquire(1) {
40//!     // allowed — do the work
41//! } else {
42//!     // denied — shed load / return 429 / back off
43//! }
44//! # }
45//! ```
46//!
47//! The bucket reads time from [`clock-lib`](https://crates.io/crates/clock-lib);
48//! the `clock` feature (on by default) provides it and implies `std`. The
49//! lock-free accounting core needs only `core`, but the shipped `Bucket`
50//! constructors read the clock, so a bare `no_std` build
51//! (`default-features = false`) exposes only [`VERSION`].
52//!
53//! ## Design goals
54//!
55//! - **Lock-free acquire.** One `compare_exchange_weak` on a packed atomic
56//!   word; no `Mutex`, no parking on the hot path.
57//! - **Allocation-free steady state.** A bucket is a small, cache-line-aligned
58//!   value with no heap tail; acquiring never allocates.
59//! - **Lazy refill.** Tokens accrue from elapsed monotonic time on access — no
60//!   timer thread, no wakeups, no watts burned while idle.
61//! - **Overflow-safe.** Every refill and capacity computation is checked or
62//!   saturating; a hostile request count or a multi-day idle gap can neither
63//!   wrap the counter nor over-fill the bucket.
64//! - **`no_std`-capable.** The core runs without the standard library; the
65//!   caller drives time when `std` is disabled.
66//!
67//! ## Feature flags
68//!
69//! | Feature | Default | Description |
70//! |---------|---------|-------------|
71//! | `std`   | yes     | Standard library. Off → `no_std`, caller drives time. |
72//! | `clock` | yes     | Pluggable [`clock-lib`](https://crates.io/crates/clock-lib) time source plus a mockable clock for deterministic tests. |
73
74// `no_std` for the library build when `std` is off, but always link `std` under
75// `test` so the unit-test harness (and dev-dependencies) have what they need.
76#![cfg_attr(all(not(feature = "std"), not(test)), no_std)]
77#![deny(warnings)]
78#![deny(missing_docs)]
79#![deny(unsafe_op_in_unsafe_fn)]
80#![deny(unused_must_use)]
81#![deny(unused_results)]
82#![deny(clippy::unwrap_used)]
83#![deny(clippy::expect_used)]
84#![deny(clippy::todo)]
85#![deny(clippy::unimplemented)]
86#![deny(clippy::print_stdout)]
87#![deny(clippy::print_stderr)]
88#![deny(clippy::dbg_macro)]
89#![deny(clippy::unreachable)]
90#![deny(clippy::undocumented_unsafe_blocks)]
91
92// The token-bucket surface requires a clock to read time and a `Mutex` for the
93// simple implementation; both are gated on `clock` (which implies `std`). The
94// no_std, caller-driven core arrives with the lock-free rewrite in 0.3.
95#[cfg(feature = "clock")]
96mod bucket;
97#[cfg(feature = "clock")]
98mod builder;
99#[cfg(feature = "clock")]
100mod config;
101#[cfg(feature = "clock")]
102mod decision;
103#[cfg(feature = "clock")]
104mod error;
105
106#[cfg(feature = "clock")]
107pub use crate::bucket::{Bucket, TokenBucket};
108#[cfg(feature = "clock")]
109pub use crate::builder::BucketBuilder;
110#[cfg(feature = "clock")]
111pub use crate::config::BucketConfig;
112#[cfg(feature = "clock")]
113pub use crate::decision::Decision;
114#[cfg(feature = "clock")]
115pub use crate::error::BucketError;
116
117/// The version of this crate, taken from `Cargo.toml` at compile time.
118///
119/// Exposed so a consumer can report the exact `better-bucket` build it links
120/// against — useful in diagnostics and version-skew checks across a dependency
121/// tree.
122///
123/// # Examples
124///
125/// ```
126/// // Reports the crate version as a `major.minor.patch` string.
127/// let version = better_bucket::VERSION;
128/// assert!(version.starts_with("1."));
129/// assert_eq!(version.split('.').count(), 3);
130/// ```
131pub const VERSION: &str = env!("CARGO_PKG_VERSION");
132
133#[cfg(test)]
134mod tests {
135    use super::VERSION;
136
137    #[test]
138    fn test_version_is_well_formed_semver() {
139        // A `major.minor.patch` core with no empty components.
140        let parts: Vec<&str> = VERSION.split('.').collect();
141        assert_eq!(parts.len(), 3, "expected major.minor.patch, got {VERSION}");
142        assert!(parts.iter().all(|part| !part.is_empty()));
143    }
144}