Copied from api_gateway, removed REST handlers (port 8080), added tonic-web + CORS for grpc-web browser access. Binary renamed: api-gateway → api Changes: - Package name: api-gateway → api - Deleted src/handlers/ (REST ML endpoints on port 8080) - Added tonic-web 0.13 + tower-http CORS layer - Server::builder().accept_http1(true) for grpc-web - CORS_ORIGINS env var (default http://localhost:5173) - Metrics server on port 9091 (axum) preserved - All 95 lib tests pass, 0 clippy warnings - Added services/api to workspace members Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
321 lines
8.7 KiB
Rust
321 lines
8.7 KiB
Rust
//! Rate Limiting Performance Benchmark
|
|
//!
|
|
//! Measures rate limiter performance:
|
|
//! - TARGET: <50ns per check
|
|
//! - Atomic counter operations
|
|
//! - Token bucket algorithm
|
|
//! - Sliding window counters
|
|
//! - Concurrent access patterns
|
|
|
|
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
|
use std::collections::HashMap;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// Atomic counter-based rate limiter (fastest)
|
|
struct AtomicRateLimiter {
|
|
counters: HashMap<String, Arc<AtomicU64>>,
|
|
limit: u64,
|
|
}
|
|
|
|
impl AtomicRateLimiter {
|
|
fn new(limit: u64) -> Self {
|
|
Self {
|
|
counters: HashMap::new(),
|
|
limit,
|
|
}
|
|
}
|
|
|
|
fn check(&self, user_id: &str) -> bool {
|
|
if let Some(counter) = self.counters.get(user_id) {
|
|
let count = counter.fetch_add(1, Ordering::Relaxed);
|
|
count < self.limit
|
|
} else {
|
|
true // First request always allowed
|
|
}
|
|
}
|
|
|
|
fn with_user(mut self, user_id: &str) -> Self {
|
|
self.counters
|
|
.insert(user_id.to_string(), Arc::new(AtomicU64::new(0)));
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Token bucket rate limiter
|
|
struct TokenBucket {
|
|
tokens: Mutex<f64>,
|
|
last_refill: Mutex<Instant>,
|
|
capacity: f64,
|
|
refill_rate: f64, // tokens per second
|
|
}
|
|
|
|
impl TokenBucket {
|
|
fn new(capacity: f64, refill_rate: f64) -> Self {
|
|
Self {
|
|
tokens: Mutex::new(capacity),
|
|
last_refill: Mutex::new(Instant::now()),
|
|
capacity,
|
|
refill_rate,
|
|
}
|
|
}
|
|
|
|
fn check(&self) -> bool {
|
|
let mut tokens = self.tokens.lock().expect("INVARIANT: Lock should not be poisoned");
|
|
let mut last_refill = self.last_refill.lock().expect("INVARIANT: Lock should not be poisoned");
|
|
|
|
let now = Instant::now();
|
|
let elapsed = now.duration_since(*last_refill).as_secs_f64();
|
|
|
|
// Refill tokens
|
|
*tokens = (*tokens + (elapsed * self.refill_rate)).min(self.capacity);
|
|
*last_refill = now;
|
|
|
|
if *tokens >= 1.0 {
|
|
*tokens -= 1.0;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sliding window counter
|
|
struct SlidingWindow {
|
|
window: Mutex<Vec<Instant>>,
|
|
window_size: Duration,
|
|
limit: usize,
|
|
}
|
|
|
|
impl SlidingWindow {
|
|
fn new(window_size: Duration, limit: usize) -> Self {
|
|
Self {
|
|
window: Mutex::new(Vec::new()),
|
|
window_size,
|
|
limit,
|
|
}
|
|
}
|
|
|
|
fn check(&self) -> bool {
|
|
let mut window = self.window.lock().expect("INVARIANT: Lock should not be poisoned");
|
|
let now = Instant::now();
|
|
|
|
// Remove expired entries
|
|
window.retain(|&time| now.duration_since(time) < self.window_size);
|
|
|
|
if window.len() < self.limit {
|
|
window.push(now);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Benchmark 1: Atomic counter (TARGET: <50ns)
|
|
fn bench_atomic_rate_limiter(c: &mut Criterion) {
|
|
let limiter = AtomicRateLimiter::new(1_000_000).with_user("user123");
|
|
|
|
c.bench_function("atomic_rate_limiter", |b| {
|
|
b.iter(|| {
|
|
let allowed = limiter.check(black_box("user123"));
|
|
black_box(allowed);
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Benchmark 2: Token bucket algorithm
|
|
fn bench_token_bucket(c: &mut Criterion) {
|
|
let bucket = TokenBucket::new(100.0, 100.0);
|
|
|
|
c.bench_function("token_bucket_rate_limiter", |b| {
|
|
b.iter(|| {
|
|
let allowed = bucket.check();
|
|
black_box(allowed);
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Benchmark 3: Sliding window counter
|
|
fn bench_sliding_window(c: &mut Criterion) {
|
|
let window = SlidingWindow::new(Duration::from_secs(1), 100);
|
|
|
|
c.bench_function("sliding_window_rate_limiter", |b| {
|
|
b.iter(|| {
|
|
let allowed = window.check();
|
|
black_box(allowed);
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Benchmark 4: Different user counts
|
|
fn bench_user_scaling(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("rate_limiter_user_scaling");
|
|
|
|
for num_users in &[10, 100, 1_000, 10_000] {
|
|
let mut limiter = AtomicRateLimiter::new(1_000_000);
|
|
for i in 0..*num_users {
|
|
limiter = limiter.with_user(&format!("user{}", i));
|
|
}
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("atomic_limiter", num_users),
|
|
num_users,
|
|
|b, &n| {
|
|
b.iter(|| {
|
|
let user_id = format!("user{}", black_box(n / 2));
|
|
let allowed = limiter.check(&user_id);
|
|
black_box(allowed);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
/// Benchmark 5: Burst handling
|
|
fn bench_burst_handling(c: &mut Criterion) {
|
|
let bucket = TokenBucket::new(100.0, 100.0);
|
|
|
|
c.bench_function("burst_100_requests", |b| {
|
|
b.iter(|| {
|
|
let mut allowed_count = 0;
|
|
for _ in 0..100 {
|
|
if bucket.check() {
|
|
allowed_count += 1;
|
|
}
|
|
}
|
|
black_box(allowed_count);
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Benchmark 6: Rate limiter with refill overhead
|
|
fn bench_refill_overhead(c: &mut Criterion) {
|
|
let bucket = TokenBucket::new(1000.0, 1000.0);
|
|
|
|
let mut group = c.benchmark_group("refill_overhead");
|
|
|
|
// Simulate different request patterns
|
|
group.bench_function("high_frequency_no_wait", |b| {
|
|
b.iter(|| {
|
|
let allowed = bucket.check();
|
|
black_box(allowed);
|
|
});
|
|
});
|
|
|
|
group.bench_function("with_small_delay", |b| {
|
|
b.iter(|| {
|
|
std::thread::sleep(Duration::from_micros(1));
|
|
let allowed = bucket.check();
|
|
black_box(allowed);
|
|
});
|
|
});
|
|
|
|
group.finish();
|
|
}
|
|
|
|
/// Benchmark 7: Concurrent access to rate limiter
|
|
fn bench_concurrent_access(c: &mut Criterion) {
|
|
use std::thread;
|
|
|
|
let limiter = Arc::new(AtomicRateLimiter::new(1_000_000).with_user("user123"));
|
|
|
|
c.bench_function("concurrent_rate_limiter_4_threads", |b| {
|
|
b.iter(|| {
|
|
let mut handles = vec![];
|
|
|
|
for _ in 0..4 {
|
|
let limiter_clone = limiter.clone();
|
|
let handle = thread::spawn(move || {
|
|
for _ in 0..1000 {
|
|
black_box(limiter_clone.check("user123"));
|
|
}
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
for handle in handles {
|
|
handle.join().expect("INVARIANT: Thread should complete successfully");
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Benchmark 8: Deny rate (when limit exceeded)
|
|
fn bench_deny_rate(c: &mut Criterion) {
|
|
let limiter = AtomicRateLimiter::new(0).with_user("user123"); // Zero limit
|
|
|
|
c.bench_function("rate_limiter_deny_path", |b| {
|
|
b.iter(|| {
|
|
let allowed = limiter.check(black_box("user123"));
|
|
assert!(!allowed); // Should always deny
|
|
black_box(allowed);
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Benchmark 9: Cache hit patterns
|
|
fn bench_cache_patterns(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("cache_hit_patterns");
|
|
|
|
// Hot path: Same user repeatedly (cache hit)
|
|
let limiter_hot = AtomicRateLimiter::new(1_000_000).with_user("user123");
|
|
group.bench_function("hot_path_same_user", |b| {
|
|
b.iter(|| {
|
|
let allowed = limiter_hot.check(black_box("user123"));
|
|
black_box(allowed);
|
|
});
|
|
});
|
|
|
|
// Cold path: Different users (cache miss simulation)
|
|
let mut limiter_cold = AtomicRateLimiter::new(1_000_000);
|
|
for i in 0..1000 {
|
|
limiter_cold = limiter_cold.with_user(&format!("user{}", i));
|
|
}
|
|
let mut counter = 0;
|
|
group.bench_function("cold_path_different_users", |b| {
|
|
b.iter(|| {
|
|
counter += 1;
|
|
let user_id = format!("user{}", counter % 1000);
|
|
let allowed = limiter_cold.check(&user_id);
|
|
black_box(allowed);
|
|
});
|
|
});
|
|
|
|
group.finish();
|
|
}
|
|
|
|
/// Benchmark 10: HFT scenario (100K requests/sec target)
|
|
fn bench_hft_scenario(c: &mut Criterion) {
|
|
let limiter = AtomicRateLimiter::new(100_000).with_user("hft_trader");
|
|
|
|
c.bench_function("hft_100k_rps_scenario", |b| {
|
|
b.iter_custom(|iters| {
|
|
let start = Instant::now();
|
|
for _ in 0..iters {
|
|
black_box(limiter.check("hft_trader"));
|
|
}
|
|
start.elapsed()
|
|
});
|
|
});
|
|
}
|
|
|
|
criterion_group!(
|
|
rate_limiting_benches,
|
|
bench_atomic_rate_limiter,
|
|
bench_token_bucket,
|
|
bench_sliding_window,
|
|
bench_user_scaling,
|
|
bench_burst_handling,
|
|
bench_refill_overhead,
|
|
bench_concurrent_access,
|
|
bench_deny_rate,
|
|
bench_cache_patterns,
|
|
bench_hft_scenario
|
|
);
|
|
|
|
criterion_main!(rate_limiting_benches);
|