use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
#[derive(Clone, Debug)]
pub(crate) struct RemoteRateLimiter {
inner: Arc<RateLimiterInner>,
}
#[derive(Debug)]
struct RateLimiterInner {
tokens_milli: AtomicU64,
last_refill_ns: AtomicU64,
epoch: Instant,
refill_per_ns_milli_mega: u64,
capacity_milli: u64,
}
impl RemoteRateLimiter {
pub(crate) fn new(per_second: u64, burst: u64) -> Self {
let capacity_milli = burst.saturating_mul(1_000);
let refill_per_ns_milli_mega = per_second.saturating_mul(1_000_000);
Self {
inner: Arc::new(RateLimiterInner {
tokens_milli: AtomicU64::new(capacity_milli),
last_refill_ns: AtomicU64::new(0),
epoch: Instant::now(),
refill_per_ns_milli_mega,
capacity_milli,
}),
}
}
pub(crate) fn try_consume(&self) -> bool {
let inner = &*self.inner;
let now_ns = inner.epoch.elapsed().as_nanos().min(u64::MAX as u128) as u64;
let last_ns = inner.last_refill_ns.load(Ordering::Relaxed);
let elapsed_ns = now_ns.saturating_sub(last_ns);
if elapsed_ns > 0 {
let accrued_milli =
elapsed_ns.saturating_mul(inner.refill_per_ns_milli_mega) / 1_000_000_000;
if accrued_milli > 0 {
if inner
.last_refill_ns
.compare_exchange(last_ns, now_ns, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
let prev = inner
.tokens_milli
.fetch_add(accrued_milli, Ordering::Relaxed);
let total = prev.saturating_add(accrued_milli);
if total > inner.capacity_milli {
inner
.tokens_milli
.store(inner.capacity_milli, Ordering::Relaxed);
}
}
}
}
const ONE_TOKEN_MILLI: u64 = 1_000;
let prev = inner.tokens_milli.load(Ordering::Relaxed);
if prev < ONE_TOKEN_MILLI {
return false;
}
inner
.tokens_milli
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |t| {
t.checked_sub(ONE_TOKEN_MILLI)
})
.is_ok()
}
pub(crate) fn from_env() -> Self {
let per_sec: u64 = std::env::var("MNEM_REMOTE_RATE_PER_SEC")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(100);
let burst: u64 = std::env::var("MNEM_REMOTE_RATE_BURST")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(50);
Self::new(per_sec, burst)
}
}
pub(crate) async fn remote_rate_limit_middleware(
limiter: Arc<RemoteRateLimiter>,
req: Request<Body>,
next: Next,
) -> Response {
if limiter.try_consume() {
next.run(req).await
} else {
rate_limit_response()
}
}
fn rate_limit_response() -> Response {
let body = serde_json::json!({
"type": "https://mnem.dev/errors/remote/rate-limited",
"title": "Too Many Requests",
"status": 429,
"detail": "remote protocol rate limit exceeded; retry after 1 second",
});
(
StatusCode::TOO_MANY_REQUESTS,
[
(axum::http::header::CONTENT_TYPE, "application/problem+json"),
(axum::http::header::RETRY_AFTER, "1"),
],
body.to_string(),
)
.into_response()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_bucket_allows_burst_requests() {
let limiter = RemoteRateLimiter::new(10, 5);
for i in 0..5 {
assert!(
limiter.try_consume(),
"request {i} should be allowed from a full bucket"
);
}
assert!(
!limiter.try_consume(),
"6th request should be denied (bucket exhausted)"
);
}
#[test]
fn empty_bucket_refills_after_elapsed_time() {
let limiter = RemoteRateLimiter::new(1_000_000, 3);
assert!(limiter.try_consume());
assert!(limiter.try_consume());
assert!(limiter.try_consume());
assert!(!limiter.try_consume());
let two_seconds_ago_ns = limiter
.inner
.epoch
.elapsed()
.as_nanos()
.min(u64::MAX as u128) as u64;
let backdated = two_seconds_ago_ns.saturating_sub(2_000_000_000);
limiter
.inner
.last_refill_ns
.store(backdated, Ordering::Relaxed);
assert!(
limiter.try_consume(),
"bucket should have refilled after simulated 2 s elapsed"
);
}
#[test]
fn from_env_uses_defaults_when_vars_absent() {
let limiter = RemoteRateLimiter::from_env();
assert_eq!(
limiter.inner.capacity_milli, 50_000,
"default burst should be 50"
);
}
}