Files
rustytorch/crates/production/rtx-serving-api/src/rate_limiting.rs
T
osobhandClaude Fable 5 0cbfc1a739
Documentation / Build API Documentation (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 7s
CI / Format Check (push) Failing after 10s
CI / Build (ubuntu-latest) (push) Failing after 29s
Performance Benchmarks / Run Benchmarks (push) Successful in 31s
CI / Test (macos-latest) (push) Has been skipped
CI / Test (ubuntu-latest) (push) Has been skipped
CI / CI Success (push) Failing after 1s
CI / Build (macos-latest) (push) Failing after 9s
CI / Python Bindings (maturin) (macos-latest) (push) Has been skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Has been skipped
CI / WASM Build + Size Check (push) Has been skipped
CI / Distributed Training Tests (push) Has been skipped
CI / Clippy Check (push) Failing after 34s
CI / Build CPU-Only (Explicit) (push) Failing after 48s
fix(tests): repair rtx-onnx-codegen build and all pre-existing test failures in rtx-serving-api and rtx-runtime
- rtx-onnx-codegen: re-export AttributeValue from ir (private-module
  import broke the whole crate; remaining errors were knock-ons).
- rtx-runtime: gate test_kernel_launch/test_kernel_statistics behind the
  cuda feature (they need a real CUDA stream; verified passing with
  --features cuda on the RTX 5060 Ti); non-cuda stream_to_cuda_handle
  error message now says "not supported" so error-propagation tests are
  valid in both build modes.
- rtx-serving-api (31 failures → 0, 192 pass): per-instance Prometheus
  registries (macros were silently registering into the global one),
  kv-cache eviction scoring at microsecond precision + memory_bytes
  actually reported, #[serde(default)] on cache config for partial TOML,
  radix-tree capacity/cleanup/prefix-length fixes, sliding-window
  context-carry fixes, speculative beam-search early-stop fix,
  CacheValue::is_expired off-by-one, n-gram double-append fix,
  grammar validation fix, deterministic health status, streaming
  no-subscriber send no longer treated as an error, websocket messages
  switched to adjacently-tagged serde (internally-tagged could not
  serialize the newtype variants at all — the old wire format errored
  at runtime for those messages; no external consumers existed since
  the serving layer was mock until this sweep), plus a handful of
  test-side numerical/formula corrections.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-09 19:49:01 -07:00

587 lines
17 KiB
Rust

//! Advanced rate limiting and token management
//!
//! Provides comprehensive rate limiting capabilities including:
//! - Per-user token quotas with sliding window tracking
//! - Organization-level rate limits with hierarchical enforcement
//! - Token cost calculation per model and operation type
//! - Dynamic rate limiting based on system load and user tier
//! - Burst allowances with exponential backoff
//! - Token usage analytics and reporting
use anyhow::Result;
use dashmap::DashMap;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Duration};
use tokio::time::Instant;
/// User tier for rate limiting
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum UserTier {
Free,
Pro,
Enterprise,
}
impl UserTier {
/// Get base rate limit multiplier
#[must_use]
pub fn rate_multiplier(self) -> f64 {
match self {
Self::Free => 1.0,
Self::Pro => 5.0,
Self::Enterprise => 20.0,
}
}
/// Get burst allowance multiplier
#[must_use]
pub fn burst_multiplier(self) -> f64 {
match self {
Self::Free => 2.0,
Self::Pro => 3.0,
Self::Enterprise => 5.0,
}
}
}
/// Token cost calculation based on model and operation
#[derive(Debug, Clone, PartialEq)]
pub struct TokenCost {
pub input_tokens: u64,
pub output_tokens: u64,
pub model_cost_multiplier: f64,
pub operation_cost_multiplier: f64,
}
impl TokenCost {
/// Calculate total cost
#[must_use]
pub fn total_cost(&self) -> f64 {
let base_cost = (self.input_tokens as f64 * 0.001) + (self.output_tokens as f64 * 0.002);
base_cost * self.model_cost_multiplier * self.operation_cost_multiplier
}
/// Calculate token usage
#[must_use]
pub fn total_tokens(&self) -> u64 {
self.input_tokens + self.output_tokens
}
}
/// Sliding window for rate limiting
#[derive(Debug, Clone)]
pub struct SlidingWindow {
window_size: Duration,
buckets: Vec<u64>,
bucket_duration: Duration,
last_update: Instant,
}
impl SlidingWindow {
/// Create new sliding window
#[must_use]
pub fn new(window_size: Duration, bucket_count: usize) -> Self {
let bucket_duration = window_size / bucket_count as u32;
Self {
window_size,
buckets: vec![0; bucket_count],
bucket_duration,
last_update: Instant::now(),
}
}
/// Update window and add tokens
pub fn add_tokens(&mut self, tokens: u64) {
self.update_buckets();
let current_bucket = self.buckets.len() - 1;
self.buckets[current_bucket] += tokens;
}
/// Get current usage in window
pub fn current_usage(&mut self) -> u64 {
self.update_buckets();
self.buckets.iter().sum()
}
/// Update buckets based on elapsed time
fn update_buckets(&mut self) {
let now = Instant::now();
let elapsed = now.duration_since(self.last_update);
let buckets_to_advance = (elapsed.as_millis() / self.bucket_duration.as_millis()) as usize;
if buckets_to_advance > 0 {
// Shift buckets
let advance = buckets_to_advance.min(self.buckets.len());
self.buckets.rotate_left(advance);
// Clear advanced buckets
let clear_start = self.buckets.len().saturating_sub(advance);
for bucket in &mut self.buckets[clear_start..] {
*bucket = 0;
}
self.last_update = now;
}
}
}
/// User rate limit state
#[derive(Debug)]
pub struct UserRateLimit {
pub user_id: String,
pub organization_id: Option<String>,
pub tier: UserTier,
pub token_window: Arc<RwLock<SlidingWindow>>,
pub request_window: Arc<RwLock<SlidingWindow>>,
pub last_request: Instant,
pub consecutive_limits: u32,
pub burst_tokens_used: u64,
pub total_cost: f64,
}
impl UserRateLimit {
/// Create new user rate limit
#[must_use]
pub fn new(user_id: String, organization_id: Option<String>, tier: UserTier) -> Self {
let token_window = Arc::new(RwLock::new(SlidingWindow::new(Duration::from_secs(60), 60)));
let request_window = Arc::new(RwLock::new(SlidingWindow::new(Duration::from_secs(60), 60)));
Self {
user_id,
organization_id,
tier,
token_window,
request_window,
last_request: Instant::now(),
consecutive_limits: 0,
burst_tokens_used: 0,
total_cost: 0.0,
}
}
/// Check if request is allowed
pub fn check_rate_limit(&mut self, cost: &TokenCost) -> Result<bool> {
let now = Instant::now();
// Calculate limits based on tier
let base_token_limit = 1000_u64;
let base_request_limit = 60_u64;
let token_limit = (base_token_limit as f64 * self.tier.rate_multiplier()) as u64;
let request_limit = (base_request_limit as f64 * self.tier.rate_multiplier()) as u64;
// Check request limit
{
let mut request_window = self.request_window.write();
let current_requests = request_window.current_usage();
if current_requests >= request_limit {
self.consecutive_limits += 1;
return Ok(false);
}
}
// Check token limit
{
let mut token_window = self.token_window.write();
let current_tokens = token_window.current_usage();
let required_tokens = cost.total_tokens();
if current_tokens + required_tokens > token_limit {
// Check burst allowance
let burst_limit = (token_limit as f64 * self.tier.burst_multiplier()) as u64;
if current_tokens + required_tokens > burst_limit {
self.consecutive_limits += 1;
return Ok(false);
}
self.burst_tokens_used += required_tokens;
}
}
// Apply exponential backoff for consecutive limits
if self.consecutive_limits > 0 {
let backoff_duration = Duration::from_millis(100 * 2_u64.pow(self.consecutive_limits));
let time_since_last = now.duration_since(self.last_request);
if time_since_last < backoff_duration {
return Ok(false);
}
}
// Request allowed - update counters
{
let mut token_window = self.token_window.write();
token_window.add_tokens(cost.total_tokens());
}
{
let mut request_window = self.request_window.write();
request_window.add_tokens(1);
}
self.last_request = now;
self.consecutive_limits = 0;
self.total_cost += cost.total_cost();
Ok(true)
}
/// Get current usage statistics
pub fn get_usage_stats(&mut self) -> UserUsageStats {
let token_usage = {
let mut token_window = self.token_window.write();
token_window.current_usage()
};
let request_usage = {
let mut request_window = self.request_window.write();
request_window.current_usage()
};
UserUsageStats {
user_id: self.user_id.clone(),
tier: self.tier,
tokens_used: token_usage,
requests_made: request_usage,
burst_tokens_used: self.burst_tokens_used,
total_cost: self.total_cost,
consecutive_limits: self.consecutive_limits,
}
}
}
/// Organization rate limit state
#[derive(Debug)]
pub struct OrganizationRateLimit {
pub organization_id: String,
pub token_window: Arc<RwLock<SlidingWindow>>,
pub request_window: Arc<RwLock<SlidingWindow>>,
pub total_cost: f64,
pub user_limits: DashMap<String, Arc<RwLock<UserRateLimit>>>,
}
impl OrganizationRateLimit {
/// Create new organization rate limit
#[must_use]
pub fn new(organization_id: String) -> Self {
let token_window = Arc::new(RwLock::new(SlidingWindow::new(
Duration::from_secs(3600),
60,
)));
let request_window = Arc::new(RwLock::new(SlidingWindow::new(
Duration::from_secs(3600),
60,
)));
Self {
organization_id,
token_window,
request_window,
total_cost: 0.0,
user_limits: DashMap::new(),
}
}
/// Check organization-level limits
pub fn check_organization_limit(&mut self, cost: &TokenCost) -> Result<bool> {
// Organization limits (much higher than individual)
let org_token_limit = 1_000_000_u64;
let org_request_limit = 10_000_u64;
// Check request limit
{
let mut request_window = self.request_window.write();
let current_requests = request_window.current_usage();
if current_requests >= org_request_limit {
return Ok(false);
}
}
// Check token limit
{
let mut token_window = self.token_window.write();
let current_tokens = token_window.current_usage();
if current_tokens + cost.total_tokens() > org_token_limit {
return Ok(false);
}
// Update usage
token_window.add_tokens(cost.total_tokens());
}
{
let mut request_window = self.request_window.write();
request_window.add_tokens(1);
}
self.total_cost += cost.total_cost();
Ok(true)
}
}
/// Rate limiting manager
pub struct RateLimitManager {
user_limits: DashMap<String, Arc<RwLock<UserRateLimit>>>,
organization_limits: DashMap<String, Arc<RwLock<OrganizationRateLimit>>>,
system_load_factor: Arc<RwLock<f64>>,
}
impl Default for RateLimitManager {
fn default() -> Self {
Self::new()
}
}
impl RateLimitManager {
/// Create new rate limit manager
#[must_use]
pub fn new() -> Self {
Self {
user_limits: DashMap::new(),
organization_limits: DashMap::new(),
system_load_factor: Arc::new(RwLock::new(1.0)),
}
}
/// Check rate limits for a request
pub async fn check_rate_limit(
&self,
user_id: &str,
organization_id: Option<&str>,
tier: UserTier,
cost: &TokenCost,
) -> Result<bool> {
// Apply system load factor
let load_factor = *self.system_load_factor.read();
let adjusted_cost = TokenCost {
input_tokens: (cost.input_tokens as f64 * load_factor) as u64,
output_tokens: (cost.output_tokens as f64 * load_factor) as u64,
model_cost_multiplier: cost.model_cost_multiplier,
operation_cost_multiplier: cost.operation_cost_multiplier,
};
// Check organization limits first if applicable
if let Some(org_id) = organization_id {
let org_limit = self
.organization_limits
.entry(org_id.to_string())
.or_insert_with(|| {
Arc::new(RwLock::new(OrganizationRateLimit::new(org_id.to_string())))
});
let mut org_limit = org_limit.write();
if !org_limit.check_organization_limit(&adjusted_cost)? {
return Ok(false);
}
}
// Check user limits
let user_limit = self
.user_limits
.entry(user_id.to_string())
.or_insert_with(|| {
Arc::new(RwLock::new(UserRateLimit::new(
user_id.to_string(),
organization_id.map(String::from),
tier,
)))
});
let mut user_limit = user_limit.write();
user_limit.check_rate_limit(&adjusted_cost)
}
/// Update system load factor
pub fn update_system_load(&self, load_factor: f64) {
*self.system_load_factor.write() = load_factor.clamp(0.1, 10.0);
}
/// Get usage statistics for a user
#[must_use]
pub fn get_user_usage(&self, user_id: &str) -> Option<UserUsageStats> {
self.user_limits.get(user_id).map(|limit| {
let mut limit = limit.write();
limit.get_usage_stats()
})
}
/// Get organization usage statistics
#[must_use]
pub fn get_organization_usage(&self, organization_id: &str) -> Option<OrganizationUsageStats> {
self.organization_limits.get(organization_id).map(|limit| {
let limit = limit.read();
let token_usage = {
let mut token_window = limit.token_window.write();
token_window.current_usage()
};
let request_usage = {
let mut request_window = limit.request_window.write();
request_window.current_usage()
};
OrganizationUsageStats {
organization_id: limit.organization_id.clone(),
tokens_used: token_usage,
requests_made: request_usage,
total_cost: limit.total_cost,
user_count: limit.user_limits.len(),
}
})
}
/// Clear expired rate limit entries
pub async fn cleanup_expired_entries(&self) {
// Remove users that haven't made requests in the last hour
let cutoff = Instant::now() - Duration::from_secs(3600);
self.user_limits.retain(|_, limit| {
let limit = limit.read();
limit.last_request > cutoff
});
}
}
/// User usage statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserUsageStats {
pub user_id: String,
pub tier: UserTier,
pub tokens_used: u64,
pub requests_made: u64,
pub burst_tokens_used: u64,
pub total_cost: f64,
pub consecutive_limits: u32,
}
/// Organization usage statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrganizationUsageStats {
pub organization_id: String,
pub tokens_used: u64,
pub requests_made: u64,
pub total_cost: f64,
pub user_count: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_sliding_window_basic() {
let mut window = SlidingWindow::new(Duration::from_millis(100), 10);
window.add_tokens(10);
assert_eq!(window.current_usage(), 10);
window.add_tokens(5);
assert_eq!(window.current_usage(), 15);
}
#[test]
fn test_sliding_window_expiry() {
let mut window = SlidingWindow::new(Duration::from_millis(50), 5);
window.add_tokens(10);
assert_eq!(window.current_usage(), 10);
// Wait for window to expire
thread::sleep(Duration::from_millis(60));
assert_eq!(window.current_usage(), 0);
}
#[test]
fn test_token_cost_calculation() {
let cost = TokenCost {
input_tokens: 100,
output_tokens: 50,
model_cost_multiplier: 2.0,
operation_cost_multiplier: 1.5,
};
assert_eq!(cost.total_tokens(), 150);
// (100*0.001 + 50*0.002) * 2.0 * 1.5 = 0.6
assert!((cost.total_cost() - 0.6).abs() < 1e-9);
}
#[test]
fn test_user_tier_multipliers() {
assert_eq!(UserTier::Free.rate_multiplier(), 1.0);
assert_eq!(UserTier::Pro.rate_multiplier(), 5.0);
assert_eq!(UserTier::Enterprise.rate_multiplier(), 20.0);
}
#[tokio::test]
async fn test_rate_limit_manager() {
let manager = RateLimitManager::new();
let cost = TokenCost {
input_tokens: 10,
output_tokens: 5,
model_cost_multiplier: 1.0,
operation_cost_multiplier: 1.0,
};
// Should allow first request
assert!(
manager
.check_rate_limit("user1", None, UserTier::Free, &cost)
.await
.unwrap()
);
// Should still allow reasonable usage
for _ in 0..50 {
assert!(
manager
.check_rate_limit("user1", None, UserTier::Free, &cost)
.await
.unwrap()
);
}
}
#[tokio::test]
async fn test_organization_limits() {
let manager = RateLimitManager::new();
let cost = TokenCost {
input_tokens: 10,
output_tokens: 5,
model_cost_multiplier: 1.0,
operation_cost_multiplier: 1.0,
};
// Should allow requests under organization limit
assert!(
manager
.check_rate_limit("user1", Some("org1"), UserTier::Free, &cost)
.await
.unwrap()
);
}
#[test]
fn test_user_rate_limit_creation() {
let user_limit =
UserRateLimit::new("user1".to_string(), Some("org1".to_string()), UserTier::Pro);
assert_eq!(user_limit.user_id, "user1");
assert_eq!(user_limit.organization_id, Some("org1".to_string()));
assert_eq!(user_limit.tier, UserTier::Pro);
}
#[test]
fn test_organization_rate_limit_creation() {
let org_limit = OrganizationRateLimit::new("org1".to_string());
assert_eq!(org_limit.organization_id, "org1");
assert_eq!(org_limit.total_cost, 0.0);
}
}