Governor Rate Limit Middleware

Apply the governor quota-based limiter with per-key direct limiters, burst capacity, and 429 responses carrying retry timing.

RustResilience

Code

snippet
use governor::{
    clock::DefaultClock, middleware::NoOpMiddleware, state::direct::NotKeyed,
    state::InMemoryState, Quota, RateLimiter,
};
use std::num::NonZeroU32;
use std::sync::Arc;
use std::time::Duration;

type SharedLimiter = RateLimiter<
    NotKeyed,
    InMemoryState,
    DefaultClock,
    NoOpMiddleware,
>;

#[derive(Clone)]
struct ApiGate {
    limiter: Arc<SharedLimiter>,
}

impl ApiGate {
    fn new(per_minute: u32) -> Self {
        let quota = Quota::per_minute(NonZeroU32::new(per_minute).expect("positive"));
        Self {
            limiter: Arc::new(RateLimiter::direct(quota)),
        }
    }

    fn check(&self) -> Result<(), Duration> {
        match self.limiter.check() {
            Ok(_) => Ok(()),
            Err(negative) => Err(negative.wait_time_from_now()),
        }
    }
}

async fn handle_request(
    gate: &ApiGate,
    route: &str,
) -> Result<String, (u16, String)> {
    if let Err(wait) = gate.check() {
        let retry_after = wait.as_secs().max(1);
        return Err((
            429,
            format!("rate limited, retry after {retry_after}s"),
        ));
    }
    Ok(format!("{route} served"))
}

#[tokio::main]
async fn main() {
    let gate = ApiGate::new(5);

    let mut results = Vec::new();
    for i in 0..8 {
        let outcome = handle_request(&gate, "/search").await;
        let rendered = match outcome {
            Ok(body) => body,
            Err((status, message)) => format!("HTTP {status}: {message}"),
        };
        results.push(format!("req {i}: {rendered}"));
    }

    for line in results {
        println!("{line}");
    }
}