# cloudiful-rate-limiter
`cloudiful-rate-limiter` is an async throttling library for keyed resource access.
It answers one question: can this key access a resource now, and if not, how long should the caller wait?
The published package name is `cloudiful-rate-limiter`, while the Rust library import is `rate_limiter`.
Links:
- Gitea: <https://gitea.cloud1ful.com/crates/rate-limiter>
- docs.rs: <https://docs.rs/cloudiful-rate-limiter>
- crates.io: <https://crates.io/crates/cloudiful-rate-limiter>
Version `0.1.0` exposes:
- `RateLimitPolicy::MinInterval` for fixed minimum gaps between accesses
- `RateLimitPolicy::PerMinute` for evenly spaced requests per minute
- `LocalRateLimiter` for in-process throttling
- optional `ValkeyRateLimiter` for shared throttling across instances
- `acquire`, `try_acquire`, and `peek` APIs
Non-goals for this crate:
- job scheduling
- retries or backoff policies
- token buckets
- concurrency semaphores
- HTTP middleware
- business-specific key derivation
## Add the crate
```toml
[dependencies]
rate_limiter = { package = "cloudiful-rate-limiter", version = "0.1.0" }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
```
Enable shared Valkey backend:
```toml
[dependencies]
rate_limiter = { package = "cloudiful-rate-limiter", version = "0.1.0", features = ["valkey"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
```
## Core concepts
- `policy`: throttling rule
- `key`: explicit caller-provided scope such as `eastmoney:quote`
- `backend`: state store implementation
- `acquire`: consume one access slot, sleeping internally if needed
- `try_acquire`: consume one slot only if currently allowed
- `peek`: inspect without consuming a slot
This crate does not decide when a job starts. A scheduler, worker, CLI, or HTTP client controls that part.
## Example: local minimum interval
```rust
use std::time::Duration;
use rate_limiter::{LocalRateLimiter, RateLimiter, RateLimitPolicy};
#[tokio::main]
async fn main() {
let limiter = LocalRateLimiter::new(
RateLimitPolicy::min_interval(Duration::from_millis(500)).unwrap(),
)
.unwrap();
limiter.acquire("eastmoney:quote").await.unwrap();
limiter.acquire("eastmoney:quote").await.unwrap();
}
```
## Example: per-minute policy
```rust
use rate_limiter::RateLimitPolicy;
use std::time::Duration;
let interval = RateLimitPolicy::per_minute(120).unwrap().interval().unwrap();
assert_eq!(interval, Duration::from_millis(500));
```
## Example: shared Valkey limiter
```rust
use std::time::Duration;
use rate_limiter::{RateLimiter, RateLimitPolicy, ValkeyRateLimiter};
#[tokio::main]
async fn main() {
let limiter = ValkeyRateLimiter::new(
"redis://127.0.0.1/",
"market-data:",
RateLimitPolicy::min_interval(Duration::from_secs(1)).unwrap(),
)
.await
.unwrap();
limiter.acquire("eastmoney:quote").await.unwrap();
}
```
## Backend semantics
`LocalRateLimiter` uses process-local monotonic `Instant`.
`ValkeyRateLimiter` stores `key -> next_allowed_timestamp_ms` and uses one Lua script so competing instances observe atomic acquire decisions.
`PerMinute` currently maps to evenly spaced slots. It is not a token bucket.
## Valkey integration tests
The Valkey integration tests are marked `ignored` so default CI stays hermetic. Run them explicitly with a reachable server:
```bash
RATE_LIMITER_VALKEY_URL=redis://127.0.0.1:6379/ cargo test --features valkey --test valkey_rate_limiter -- --ignored
```
In Gitea Actions, set the `RATE_LIMITER_VALKEY_URL` secret to enable the external Valkey integration test step in `.gitea/workflows/ci.yml`.