- 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>
414 lines
13 KiB
Rust
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);
|