Files
foxhunt/testing/service-load/src/clients/trading_client.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

212 lines
6.5 KiB
Rust

//! Trading service gRPC client for load testing
use anyhow::{Context, Result};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use serde_json::json;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::Request;
use crate::metrics::LoadTestMetrics;
// Generated proto code
pub mod trading {
tonic::include_proto!("trading");
}
use trading::{
trading_service_client::TradingServiceClient, OrderSide, OrderType, StreamMarketDataRequest,
SubmitOrderRequest,
};
const TEST_ACCOUNT_ID: &str = "load-test-account";
const JWT_SECRET: &str =
"YZg5/mpqzH0NehGJXiR1yUgUg74HqdOUj/q9tnVSX+gqZvuzHKI1n0NhL4yP8CkUx7WyrVs3X86OSSxIUA6sxQ==";
pub struct TradingClient {
client: TradingServiceClient<Channel>,
jwt_token: String,
}
impl TradingClient {
pub async fn connect(url: &str) -> Result<Self> {
let channel = Channel::from_shared(url.to_string())
.context("Invalid URL")?
.connect()
.await
.context("Failed to connect to trading service")?;
// Generate JWT token for authentication
let jwt_token = Self::generate_jwt()?;
Ok(Self {
client: TradingServiceClient::new(channel),
jwt_token,
})
}
fn generate_jwt() -> Result<String> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let uuid = uuid::Uuid::new_v4();
let claims = json!({
"jti": format!("load-test-{uuid}"),
"sub": "load_test_user",
"iat": now,
"exp": now + 3600_u64,
"iss": "foxhunt-trading",
"aud": "trading-api",
"roles": ["trader"],
"permissions": ["trading.submit_order", "trading.stream_market_data"],
"token_type": "access",
"session_id": "load-test-session"
});
let header = Header::new(Algorithm::HS256);
let key = EncodingKey::from_secret(JWT_SECRET.as_ref());
encode(&header, &claims, &key).context("Failed to encode JWT")
}
pub async fn submit_test_order(
&mut self,
client_id: usize,
_order_id: usize,
) -> Result<Duration> {
let start = Instant::now();
let test_id = client_id % 100;
let order_request = SubmitOrderRequest {
symbol: format!("TEST{test_id:04}"),
side: (OrderSide::Buy as i32),
quantity: 100.0,
order_type: (OrderType::Market as i32),
price: None,
stop_price: None,
account_id: TEST_ACCOUNT_ID.to_string(),
metadata: HashMap::new(),
};
let mut request = Request::new(order_request);
// Add JWT token to metadata
let jwt_token = &self.jwt_token;
let token_value =
MetadataValue::try_from(format!("Bearer {jwt_token}")).context("Invalid JWT token")?;
request.metadata_mut().insert("authorization", token_value);
let _ = self.client.submit_order(request).await?;
Ok(start.elapsed())
}
pub async fn run_order_workload(
&mut self,
client_id: usize,
duration_secs: u64,
metrics: &LoadTestMetrics,
) -> Result<()> {
let start = Instant::now();
let mut order_count = 0;
while start.elapsed().as_secs() < duration_secs {
match self.submit_test_order(client_id, order_count).await {
Ok(duration) => metrics.record_request(duration, true),
Err(e) => {
tracing::warn!("Order failed: {:?}", e);
metrics.record_request(Duration::from_micros(0), false);
},
}
order_count += 1;
// Rate limiting: ~100 orders/sec per client
tokio::time::sleep(Duration::from_micros(10_000)).await;
}
Ok(())
}
pub async fn run_burst_workload(
&mut self,
client_id: usize,
duration_secs: u64,
metrics: &LoadTestMetrics,
) -> Result<()> {
let start = Instant::now();
let mut order_count = 0;
while start.elapsed().as_secs() < duration_secs {
match self.submit_test_order(client_id, order_count).await {
Ok(duration) => metrics.record_request(duration, true),
Err(e) => {
tracing::warn!("Burst order failed: {:?}", e);
metrics.record_request(Duration::from_micros(0), false);
},
}
order_count += 1;
// Minimal delay for burst (~100 orders/sec)
tokio::time::sleep(Duration::from_micros(10_000)).await;
}
Ok(())
}
pub async fn run_streaming_workload(
&mut self,
stream_id: usize,
duration_secs: u64,
max_updates: usize,
metrics: &LoadTestMetrics,
update_count: &AtomicUsize,
) -> Result<()> {
let stream_sym_id = stream_id % 100;
let symbols = vec![format!("STREAM{stream_sym_id:04}")];
let stream_request = StreamMarketDataRequest {
symbols,
data_types: vec![], // Empty means all data types
};
let mut request = Request::new(stream_request);
// Add JWT token to metadata
let jwt_token = &self.jwt_token;
let token_value =
MetadataValue::try_from(format!("Bearer {jwt_token}")).context("Invalid JWT token")?;
request.metadata_mut().insert("authorization", token_value);
let start = Instant::now();
match self.client.stream_market_data(request).await {
Ok(response) => {
let mut stream = response.into_inner();
let mut count = 0;
while let Ok(Some(_event)) = stream.message().await {
count += 1;
update_count.fetch_add(1_usize, Ordering::Relaxed);
if count >= max_updates || start.elapsed().as_secs() >= duration_secs {
break;
}
}
metrics.record_request(start.elapsed(), true);
},
Err(e) => {
tracing::warn!("Stream {} failed: {:?}", stream_id, e);
metrics.record_request(start.elapsed(), false);
},
}
Ok(())
}
}