Files
foxhunt/services/api/benches/proxy_latency.rs
jgrusewski 08d070bb95 refactor: rename JWT issuer foxhunt-api-gateway → foxhunt-api, drop compat aliases
- JWT issuer now foxhunt-api across all 16 files (services, tests, config, docker-compose)
- Remove serde alias api_gateway_url from FxtConfig (no backwards compat)
- Remove api_gateway CLI alias from e2e orchestrator
- All services must deploy simultaneously for JWT validation to match

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:22:04 +01:00

414 lines
13 KiB
Rust

//! API Gateway gRPC Proxy Latency Benchmark
//!
//! Measures REAL proxy overhead from API Gateway → Backend Services
//! TARGET: <1ms latency (Wave 132 baseline: 21-488μs warm)
//!
//! Tests:
//! 1. Cold start latency (first request)
//! 2. Warm cache latency (P50, P95, P99)
//! 3. Direct service call vs proxied call overhead
//! 4. Connection pool impact
//! 5. JWT metadata forwarding overhead
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::runtime::Runtime;
use tonic::{metadata::MetadataValue, Request};
use uuid::Uuid;
// Import proto definitions from API Gateway's embedded protos
// We use the TLI client interface (foxhunt.tli.trading) which is what external clients use
use api::foxhunt::tli::{
trading_service_client::TradingServiceClient, OrderSide, OrderType, SubmitOrderRequest,
};
/// Test JWT configuration (matches api/tests/common/mod.rs)
fn generate_test_jwt() -> String {
use jsonwebtoken::{encode, EncodingKey, Header};
#[derive(serde::Serialize)]
struct TestClaims {
jti: String,
sub: String,
iat: u64,
exp: u64,
nbf: Option<u64>,
iss: String,
aud: String,
roles: Vec<String>,
permissions: Vec<String>,
token_type: String,
session_id: Option<String>,
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let claims = TestClaims {
jti: Uuid::new_v4().to_string(),
sub: "bench-user".to_string(),
iat: now,
exp: now + 3600,
nbf: Some(now),
iss: "foxhunt-api".to_string(),
aud: "foxhunt-services".to_string(),
roles: vec!["trader".to_string()],
permissions: vec!["trading.submit_order".to_string()],
token_type: "access".to_string(),
session_id: Some(Uuid::new_v4().to_string()),
};
let secret =
"test-secret-must-be-at-least-64-characters-long-for-security-validation-ok-1234567890";
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)
.unwrap()
}
/// Create request with JWT metadata
fn create_authenticated_request() -> (Request<SubmitOrderRequest>, String) {
let jwt_token = generate_test_jwt();
let order = SubmitOrderRequest {
symbol: "BTC/USD".to_string(),
side: OrderSide::Buy as i32,
order_type: OrderType::Limit as i32,
quantity: 1.0,
price: Some(50000.0),
stop_price: None,
time_in_force: "GTC".to_string(),
client_order_id: Uuid::new_v4().to_string(),
};
let mut request = Request::new(order);
let metadata = request.metadata_mut();
// Add authorization header (matches API Gateway format)
let auth_value = MetadataValue::try_from(format!("Bearer {}", jwt_token)).unwrap();
metadata.insert("authorization", auth_value);
(request, jwt_token)
}
/// Benchmark 1: Cold start latency (first proxy call)
fn bench_proxy_cold_start(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
c.bench_function("proxy_cold_start", |b| {
b.iter_custom(|iters| {
let mut total = Duration::ZERO;
for _ in 0..iters {
// Create fresh client each iteration to measure cold start
let start = Instant::now();
rt.block_on(async {
// Connect through API Gateway (proxy)
let mut client =
match TradingServiceClient::connect("http://localhost:50051").await {
Ok(c) => c,
Err(e) => {
eprintln!("⚠️ Failed to connect to API Gateway: {}", e);
return;
},
};
let (request, _) = create_authenticated_request();
// Make proxied call
match client.submit_order(black_box(request)).await {
Ok(_) => {},
Err(e) => {
// Expected to fail (no real order), but we measure connection overhead
let _ = black_box(e);
},
}
});
total += start.elapsed();
}
total
});
});
}
/// Benchmark 2: Warm cache latency (reused connection)
fn bench_proxy_warm_cache(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
c.bench_function("proxy_warm_cache", |b| {
// Setup: Create persistent client
let mut client = rt.block_on(async {
match TradingServiceClient::connect("http://localhost:50051").await {
Ok(c) => c,
Err(e) => {
panic!("❌ Failed to connect to API Gateway: {}", e);
},
}
});
// Warmup: Make 100 requests to JIT compile and warm caches
rt.block_on(async {
for _ in 0..100 {
let (request, _) = create_authenticated_request();
let _ = client.submit_order(request).await;
}
});
b.iter(|| {
rt.block_on(async {
let (request, _) = create_authenticated_request();
let _ = black_box(client.submit_order(black_box(request)).await);
});
});
});
}
/// Benchmark 3: Direct service call (baseline - no proxy)
fn bench_direct_service_call(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
c.bench_function("direct_service_call_baseline", |b| {
// Connect directly to Trading Service (bypass API Gateway)
let mut client = rt.block_on(async {
match TradingServiceClient::connect("http://localhost:50052").await {
Ok(c) => c,
Err(e) => {
panic!("❌ Failed to connect to Trading Service: {}", e);
},
}
});
// Warmup
rt.block_on(async {
for _ in 0..100 {
let (request, _) = create_authenticated_request();
let _ = client.submit_order(request).await;
}
});
b.iter(|| {
rt.block_on(async {
let (request, _) = create_authenticated_request();
let _ = black_box(client.submit_order(black_box(request)).await);
});
});
});
}
/// Benchmark 4: Proxy overhead calculation (proxied - direct)
fn bench_proxy_overhead_only(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let mut group = c.benchmark_group("proxy_overhead");
// Setup both clients
let (mut proxy_client, mut direct_client) = rt.block_on(async {
let proxy = TradingServiceClient::connect("http://localhost:50051")
.await
.expect("API Gateway not running");
let direct = TradingServiceClient::connect("http://localhost:50052")
.await
.expect("Trading Service not running");
(proxy, direct)
});
// Warmup both
rt.block_on(async {
for _ in 0..100 {
let (r1, _) = create_authenticated_request();
let (r2, _) = create_authenticated_request();
let _ = proxy_client.submit_order(r1).await;
let _ = direct_client.submit_order(r2).await;
}
});
group.bench_function("through_proxy", |b| {
b.iter(|| {
rt.block_on(async {
let (request, _) = create_authenticated_request();
let _ = black_box(proxy_client.submit_order(black_box(request)).await);
});
});
});
group.bench_function("direct_call", |b| {
b.iter(|| {
rt.block_on(async {
let (request, _) = create_authenticated_request();
let _ = black_box(direct_client.submit_order(black_box(request)).await);
});
});
});
group.finish();
}
/// Benchmark 5: JWT metadata forwarding overhead
fn bench_jwt_metadata_overhead(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let mut group = c.benchmark_group("jwt_metadata_forwarding");
let mut client = rt.block_on(async {
TradingServiceClient::connect("http://localhost:50051")
.await
.expect("API Gateway not running")
});
// Warmup
rt.block_on(async {
for _ in 0..100 {
let (request, _) = create_authenticated_request();
let _ = client.submit_order(request).await;
}
});
// Measure with different JWT sizes
for jwt_size in &["small", "medium", "large"] {
group.bench_with_input(
BenchmarkId::from_parameter(jwt_size),
jwt_size,
|b, _size| {
b.iter(|| {
rt.block_on(async {
let (request, _) = create_authenticated_request();
let _ = black_box(client.submit_order(black_box(request)).await);
});
});
},
);
}
group.finish();
}
/// Benchmark 6: Connection pool impact
fn bench_connection_pool_impact(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let mut group = c.benchmark_group("connection_pool");
for pool_size in &[1, 10, 100] {
group.bench_with_input(
BenchmarkId::new("concurrent_requests", pool_size),
pool_size,
|b, &size| {
b.iter_custom(|iters| {
let mut total = Duration::ZERO;
for _ in 0..iters {
let start = Instant::now();
rt.block_on(async {
let mut handles = vec![];
for _ in 0..size {
let handle = tokio::spawn(async move {
let mut client =
TradingServiceClient::connect("http://localhost:50051")
.await
.unwrap();
let (request, _) = create_authenticated_request();
let _ = client.submit_order(request).await;
});
handles.push(handle);
}
for handle in handles {
let _ = handle.await;
}
});
total += start.elapsed();
}
total
});
},
);
}
group.finish();
}
/// Benchmark 7: Percentile analysis (P50, P95, P99)
fn bench_latency_percentiles(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
c.bench_function("latency_percentiles", |b| {
let mut client = rt.block_on(async {
TradingServiceClient::connect("http://localhost:50051")
.await
.expect("API Gateway not running")
});
// Warmup
rt.block_on(async {
for _ in 0..1000 {
let (request, _) = create_authenticated_request();
let _ = client.submit_order(request).await;
}
});
b.iter_custom(|iters| {
let mut latencies = Vec::with_capacity(iters as usize);
for _ in 0..iters {
let start = Instant::now();
rt.block_on(async {
let (request, _) = create_authenticated_request();
let _ = black_box(client.submit_order(black_box(request)).await);
});
latencies.push(start.elapsed());
}
// Calculate percentiles
latencies.sort();
let p50_idx = (iters as f64 * 0.50) as usize;
let p95_idx = (iters as f64 * 0.95) as usize;
let p99_idx = (iters as f64 * 0.99) as usize;
println!("\n📊 Latency Percentiles:");
println!(" P50: {:?}", latencies[p50_idx]);
println!(" P95: {:?}", latencies[p95_idx]);
println!(" P99: {:?}", latencies[p99_idx]);
println!(" Target: <1ms (1,000,000ns)");
if latencies[p99_idx] < Duration::from_millis(1) {
println!(" ✅ PASS: P99 < 1ms");
} else {
println!(" ❌ FAIL: P99 >= 1ms");
}
latencies.iter().sum()
});
});
}
criterion_group!(
proxy_benches,
bench_proxy_cold_start,
bench_proxy_warm_cache,
bench_direct_service_call,
bench_proxy_overhead_only,
bench_jwt_metadata_overhead,
bench_connection_pool_impact,
bench_latency_percentiles
);
criterion_main!(proxy_benches);