cloudiful-rate-limiter 0.1.0

Reusable async resource throttling with local and Valkey-backed backends.
Documentation
# 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`.