throttle_net/lib.rs
1//! # throttle-net
2//!
3//! Outbound throttling and resilience. Where `rate-net` protects your service
4//! from being overwhelmed (inbound), throttle-net protects your service from
5//! *overwhelming* the downstreams it calls — and from being banned by them. The
6//! defining operation is therefore to **wait**, not to reject: you pace your own
7//! outbound work rather than dropping someone else's request.
8//!
9//! throttle-net does not reimplement token-bucket accounting. It consumes
10//! [`better-bucket`](https://crates.io/crates/better-bucket) for that and reads
11//! time from [`clock-lib`](https://crates.io/crates/clock-lib), then builds the
12//! waiting, cost-aware, composable surface on top. It is the outbound companion
13//! to [`rate-net`](https://crates.io/crates/rate-net).
14//!
15//! ## Status
16//!
17//! **Stable (v1.0).** The public API is frozen until 2.0. The limiter and resilience surface:
18//! the [`Limiter`] trait, the [`Throttle`] token bucket and the exact
19//! [`SlidingWindowLog`], each with a waiting cost-aware
20//! [`acquire`](Throttle::acquire); the composites — [`Hybrid`] (must pass all),
21//! [`MultiLimiter`] (multi-dimensional budgets), [`PerKey`] (independent per-key
22//! state, bounded memory), and [`Layered`] (global / per-key / per-endpoint
23//! scopes); standalone [`Retry`]/[`Backoff`] with jittered backoff and
24//! `Retry-After` parsing; the resilience layer — a `CircuitBreaker` that wraps
25//! any limiter and fails fast (`circuit-breaker` feature), and a deadline-aware,
26//! priority [`Queue`]; adaptive concurrency — an `AdaptiveLimiter` that discovers
27//! the right in-flight limit from outcome feedback (`adaptive` feature); provider
28//! integration — response-header parsers with limiter sync (`provider-headers`
29//! feature) and LLM tier `presets` (`provider-llm` feature); and observability —
30//! metrics and tracing events, feature-gated and zero-cost when off (`metrics`,
31//! `tracing` features).
32//!
33//! The waiting surface runs on either [`tokio`](crate#feature-flags) or
34//! [`smol`](crate#feature-flags) — the async code is runtime-agnostic, and you
35//! choose the timer backend by feature. With `std` off, the pure algorithm core
36//! ([`Backoff`], [`Jitter`], [`Decision`]) compiles `no_std`. Correctness is held
37//! by property tests for every limiter invariant, a `loom` model check of the
38//! lock-free slot accounting, and fuzzed parsers.
39//!
40//! ```
41//! # #[cfg(feature = "runtime")]
42//! # async fn run() -> Result<(), throttle_net::ThrottleError> {
43//! use throttle_net::Throttle;
44//!
45//! // 100 requests per second, bursting up to 100.
46//! let throttle = Throttle::per_second(100);
47//!
48//! // Pace an outbound call: returns as soon as a token is free.
49//! throttle.acquire().await?;
50//! // ... call the downstream ...
51//! # Ok(())
52//! # }
53//! ```
54//!
55//! When you would rather not wait, ask without blocking:
56//!
57//! ```
58//! # #[cfg(feature = "std")] {
59//! use throttle_net::Throttle;
60//!
61//! let throttle = Throttle::per_second(100);
62//! if throttle.try_acquire() {
63//! // a token was free — send now
64//! }
65//! # }
66//! ```
67//!
68//! ## Design goals
69//!
70//! - **Wait by default.** The Tier-1 [`acquire`](Throttle::acquire) paces the
71//! caller; [`try_acquire`](Throttle::try_acquire) is there when you need the
72//! non-blocking answer.
73//! - **Cost-aware.** Not every request weighs one unit. `acquire_with_cost(n)`
74//! takes `n` tokens at once — the basis for the multi-dimensional LLM budgets
75//! that arrive with the rest of v0.2.
76//! - **Lock-free accounting.** Each acquire is a single atomic compare-and-swap
77//! in `better-bucket`; no lock sits on the path.
78//! - **Runtime-free core, lazy refill.** Tokens accrue from a monotonic clock on
79//! access; there is no background timer thread, and the synchronous core has no
80//! async-runtime dependency.
81//! - **Composable.** Every limiter is one [`Limiter`]; composites combine them
82//! without the call site changing.
83//!
84//! ## Feature flags
85//!
86//! | Feature | Default | Description |
87//! |---------|---------|-------------|
88//! | `std` | yes | Standard library — the limiter surface. With it off the crate is `no_std` and exposes the pure algorithm core ([`Backoff`], [`Jitter`], [`Decision`]) plus [`VERSION`]. |
89//! | `tokio` | yes | tokio timer backend for the waiting [`acquire`](Throttle::acquire) surface. Implies `std`. |
90//! | `smol` | no | smol timer backend, as an alternative to `tokio`. (async-std is unsupported — it is discontinued.) |
91//!
92//! The `circuit-breaker`, `adaptive`, `provider-headers` / `provider-llm`, and
93//! `metrics` / `tracing` features are documented in `docs/API.md`, which carries
94//! the full feature matrix.
95
96// `no_std` for the library build when `std` is off, but always link `std` under
97// `test` so the unit-test harness and dev-dependencies have what they need.
98#![cfg_attr(all(not(feature = "std"), not(test)), no_std)]
99#![cfg_attr(docsrs, feature(doc_cfg))]
100#![deny(missing_docs)]
101#![forbid(unsafe_code)]
102#![deny(unused_must_use)]
103#![deny(unused_results)]
104#![deny(clippy::unwrap_used)]
105#![deny(clippy::expect_used)]
106#![deny(clippy::todo)]
107#![deny(clippy::unimplemented)]
108#![deny(clippy::print_stdout)]
109#![deny(clippy::print_stderr)]
110#![deny(clippy::dbg_macro)]
111#![deny(clippy::unreachable)]
112#![deny(clippy::undocumented_unsafe_blocks)]
113
114// The limiter surface requires the standard library (the clock-driven token
115// bucket and the domain error type). With `std` off the crate is `no_std` and
116// exposes only `VERSION`.
117#[cfg(feature = "adaptive")]
118mod adaptive;
119// `backoff` and `decision` are the `no_std`-capable algorithm core: pure types
120// and math, no clock or async. The rest of the surface requires `std`.
121mod backoff;
122#[cfg(feature = "circuit-breaker")]
123mod circuit;
124mod decision;
125#[cfg(feature = "std")]
126mod error;
127#[cfg(feature = "std")]
128mod eviction;
129#[cfg(feature = "std")]
130mod hybrid;
131#[cfg(feature = "std")]
132mod layered;
133#[cfg(feature = "std")]
134mod limiter;
135#[cfg(feature = "std")]
136mod multi;
137#[cfg(any(feature = "runtime", feature = "circuit-breaker", feature = "adaptive"))]
138mod obs;
139#[cfg(feature = "std")]
140mod perkey;
141#[cfg(feature = "provider-llm")]
142pub mod presets;
143#[cfg(feature = "provider-headers")]
144pub mod provider;
145#[cfg(feature = "runtime")]
146mod queue;
147#[cfg(feature = "std")]
148mod retry;
149#[cfg(feature = "std")]
150mod retry_after;
151#[cfg(feature = "runtime")]
152mod rt;
153#[cfg(feature = "std")]
154mod sliding;
155// Loom-aware atomics indirection for the adaptive limiter's slot accounting.
156#[cfg(feature = "adaptive")]
157mod sync;
158#[cfg(feature = "std")]
159mod throttle;
160#[cfg(feature = "std")]
161mod timeutil;
162
163#[cfg(feature = "adaptive")]
164pub use crate::adaptive::{
165 AdaptiveLimiter, AdaptiveLimiterBuilder, AdaptivePermit, AdaptiveStrategy, Aimd, Outcome, Vegas,
166};
167pub use crate::backoff::{Backoff, BackoffIter, Jitter};
168#[cfg(feature = "circuit-breaker")]
169pub use crate::circuit::{BreakerState, CircuitBreaker, CircuitBreakerBuilder, Permit, Trip};
170pub use crate::decision::Decision;
171#[cfg(feature = "std")]
172pub use crate::error::ThrottleError;
173#[cfg(feature = "std")]
174pub use crate::eviction::{DEFAULT_MAX_KEYS, Eviction};
175#[cfg(feature = "std")]
176pub use crate::hybrid::{Hybrid, HybridBuilder};
177#[cfg(feature = "std")]
178pub use crate::layered::{Layered, LayeredBuilder};
179#[cfg(feature = "std")]
180pub use crate::limiter::Limiter;
181#[cfg(feature = "std")]
182pub use crate::multi::{MultiLimiter, MultiLimiterBuilder};
183#[cfg(feature = "std")]
184pub use crate::perkey::PerKey;
185#[cfg(feature = "runtime")]
186pub use crate::queue::{Overflow, Queue, QueueBuilder};
187#[cfg(feature = "std")]
188pub use crate::retry::{Retry, RetryAction, retry_if_retryable};
189#[cfg(feature = "std")]
190pub use crate::retry_after::{parse_retry_after, parse_retry_after_at};
191#[cfg(feature = "std")]
192pub use crate::sliding::SlidingWindowLog;
193#[cfg(feature = "std")]
194pub use crate::throttle::Throttle;
195
196// The clock seam is part of the public API: [`Throttle::with_clock`] and the
197// per-key/composite `with_clock` methods take any [`Clock`], and tests drive a
198// [`ManualClock`]. Re-exported so callers need not depend on `clock-lib` directly.
199#[cfg(feature = "std")]
200pub use clock_lib::{Clock, ManualClock, SystemClock};
201
202/// The version of this crate, from `Cargo.toml`.
203///
204/// # Examples
205///
206/// ```
207/// assert!(!throttle_net::VERSION.is_empty());
208/// ```
209pub const VERSION: &str = env!("CARGO_PKG_VERSION");