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>
833 lines
30 KiB
Rust
833 lines
30 KiB
Rust
//! Comprehensive Concurrency Safety Tests for HFT System
|
|
//!
|
|
//! This module contains critical tests for verifying the Foxhunt HFT system's
|
|
//! safety under high-throughput concurrent operations. These tests ensure
|
|
//! REAL MONEY safety in production HFT environments.
|
|
//!
|
|
//! CRITICAL: These tests prevent race conditions, data corruption, and
|
|
//! financial losses in high-frequency trading scenarios.
|
|
|
|
use std::sync::{Arc, atomic::{AtomicU64, AtomicBool, Ordering}};
|
|
use std::time::{Duration, Instant};
|
|
use std::collections::HashMap;
|
|
use tokio::sync::{RwLock, Mutex, Semaphore, Barrier};
|
|
use tokio::task::JoinSet;
|
|
use futures::future::join_all;
|
|
use proptest::prelude::*;
|
|
use criterion::black_box;
|
|
|
|
/// Concurrency test configuration for high-throughput scenarios
|
|
#[derive(Debug, Clone)]
|
|
/// ConcurrencyTestConfig component.
|
|
pub struct ConcurrencyTestConfig {
|
|
pub thread_count: usize,
|
|
pub operations_per_thread: usize,
|
|
pub order_burst_size: usize,
|
|
pub market_data_rate_per_second: usize,
|
|
pub position_update_frequency_ms: u64,
|
|
pub concurrent_symbols: usize,
|
|
pub test_duration_seconds: u64,
|
|
}
|
|
|
|
impl Default for ConcurrencyTestConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
thread_count: 16,
|
|
operations_per_thread: 10_000,
|
|
order_burst_size: 100,
|
|
market_data_rate_per_second: 100_000,
|
|
position_update_frequency_ms: 1,
|
|
concurrent_symbols: 50,
|
|
test_duration_seconds: 30,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// High-throughput concurrent order processing safety test
|
|
#[tokio::test]
|
|
async fn test_concurrent_order_processing_safety() {
|
|
let config = ConcurrencyTestConfig::default();
|
|
|
|
println!("🚀 Starting Concurrent Order Processing Safety Test");
|
|
println!("📊 Config: {} threads, {} ops/thread", config.thread_count, config.operations_per_thread);
|
|
|
|
// Test concurrent order submissions
|
|
test_concurrent_order_submissions(&config).await;
|
|
|
|
// Test concurrent order modifications
|
|
test_concurrent_order_modifications(&config).await;
|
|
|
|
// Test concurrent fills processing
|
|
test_concurrent_fills_processing(&config).await;
|
|
|
|
// Test concurrent position updates
|
|
test_concurrent_position_updates(&config).await;
|
|
|
|
println!("✅ Concurrent Order Processing Safety Test Completed");
|
|
}
|
|
|
|
/// Test thousands of concurrent order submissions
|
|
async fn test_concurrent_order_submissions(config: &ConcurrencyTestConfig) {
|
|
println!("📋 Testing Concurrent Order Submissions");
|
|
|
|
let order_counter = Arc::new(AtomicU64::new(0));
|
|
let success_counter = Arc::new(AtomicU64::new(0));
|
|
let error_counter = Arc::new(AtomicU64::new(0));
|
|
let barrier = Arc::new(Barrier::new(config.thread_count));
|
|
|
|
let start_time = Instant::now();
|
|
let mut tasks = JoinSet::new();
|
|
|
|
// Spawn concurrent order submission tasks
|
|
for thread_id in 0..config.thread_count {
|
|
let order_counter = order_counter.clone();
|
|
let success_counter = success_counter.clone();
|
|
let error_counter = error_counter.clone();
|
|
let barrier = barrier.clone();
|
|
let ops_per_thread = config.operations_per_thread;
|
|
|
|
tasks.spawn(async move {
|
|
// Wait for all threads to be ready
|
|
barrier.wait().await;
|
|
|
|
for op_id in 0..ops_per_thread {
|
|
let order_id = order_counter.fetch_add(1, Ordering::Relaxed);
|
|
|
|
// Create test order with unique characteristics
|
|
let order = create_test_order(thread_id, op_id, order_id);
|
|
|
|
// Submit order concurrently
|
|
match submit_order_concurrent(order).await {
|
|
Ok(_) => {
|
|
success_counter.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
Err(_) => {
|
|
error_counter.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for all tasks to complete
|
|
while let Some(result) = tasks.join_next().await {
|
|
result.expect("Task should complete successfully");
|
|
}
|
|
|
|
let elapsed = start_time.elapsed();
|
|
let total_orders = order_counter.load(Ordering::Relaxed);
|
|
let successes = success_counter.load(Ordering::Relaxed);
|
|
let errors = error_counter.load(Ordering::Relaxed);
|
|
|
|
// Verify results
|
|
assert_eq!(total_orders, (config.thread_count * config.operations_per_thread) as u64);
|
|
assert_eq!(successes + errors, total_orders);
|
|
|
|
// Performance validation
|
|
let orders_per_second = total_orders as f64 / elapsed.as_secs_f64();
|
|
println!("📊 Orders processed: {}, Rate: {:.0} orders/sec", total_orders, orders_per_second);
|
|
|
|
// Ensure high throughput (should exceed 50k orders/sec)
|
|
assert!(orders_per_second > 50_000.0,
|
|
"Order processing rate {:.0} should exceed 50k orders/sec", orders_per_second);
|
|
|
|
// Verify no data corruption occurred
|
|
verify_order_data_integrity().await;
|
|
|
|
println!("✅ Concurrent order submissions test passed");
|
|
}
|
|
|
|
/// Test concurrent order modifications without race conditions
|
|
async fn test_concurrent_order_modifications(config: &ConcurrencyTestConfig) {
|
|
println!("📋 Testing Concurrent Order Modifications");
|
|
|
|
// Create a shared order that will be modified concurrently
|
|
let shared_order = Arc::new(RwLock::new(create_shared_test_order()));
|
|
let modification_counter = Arc::new(AtomicU64::new(0));
|
|
let barrier = Arc::new(Barrier::new(config.thread_count));
|
|
|
|
let mut tasks = JoinSet::new();
|
|
|
|
// Spawn concurrent modification tasks
|
|
for thread_id in 0..config.thread_count {
|
|
let shared_order = shared_order.clone();
|
|
let modification_counter = modification_counter.clone();
|
|
let barrier = barrier.clone();
|
|
let modifications_per_thread = config.operations_per_thread / 10; // Fewer modifications
|
|
|
|
tasks.spawn(async move {
|
|
barrier.wait().await;
|
|
|
|
for _ in 0..modifications_per_thread {
|
|
// Read current order state
|
|
let current_price = {
|
|
let order = shared_order.read().await;
|
|
order.price.unwrap_or(Price::from_f64(100.0).expect("Valid price"))
|
|
};
|
|
|
|
// Modify order (this should be atomic)
|
|
{
|
|
let mut order = shared_order.write().await;
|
|
let new_price = Price::from_f64(current_price.to_f64().expect("Valid price") + 0.01);
|
|
order.price = Some(new_price);
|
|
order.quantity = Quantity::new(order.quantity.value() + 1);
|
|
}
|
|
|
|
modification_counter.fetch_add(1, Ordering::Relaxed);
|
|
|
|
// Small delay to increase chance of race conditions if they exist
|
|
tokio::task::yield_now().await;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for all modifications to complete
|
|
while let Some(result) = tasks.join_next().await {
|
|
result.expect("Modification task should complete successfully");
|
|
}
|
|
|
|
// Verify final state consistency
|
|
let final_order = shared_order.read().await;
|
|
let total_modifications = modification_counter.load(Ordering::Relaxed);
|
|
let expected_modifications = (config.thread_count * config.operations_per_thread / 10) as u64;
|
|
|
|
assert_eq!(total_modifications, expected_modifications);
|
|
|
|
// Verify order state is consistent (no race condition artifacts)
|
|
assert!(final_order.price.is_some());
|
|
assert!(final_order.quantity.value() > 0);
|
|
|
|
// Check for data corruption indicators
|
|
let price_value = final_order.price.unwrap().to_f64();
|
|
assert!(price_value > 99.0 && price_value < 200.0,
|
|
"Price should be within reasonable range, got {}", price_value);
|
|
|
|
println!("✅ Concurrent order modifications test passed");
|
|
}
|
|
|
|
/// Test concurrent fills processing for data integrity
|
|
async fn test_concurrent_fills_processing(config: &ConcurrencyTestConfig) {
|
|
println!("📋 Testing Concurrent Fills Processing");
|
|
|
|
let total_fill_volume = Arc::new(AtomicU64::new(0));
|
|
let fill_count = Arc::new(AtomicU64::new(0));
|
|
let barrier = Arc::new(Barrier::new(config.thread_count));
|
|
|
|
let mut tasks = JoinSet::new();
|
|
|
|
// Spawn concurrent fill processing tasks
|
|
for thread_id in 0..config.thread_count {
|
|
let total_fill_volume = total_fill_volume.clone();
|
|
let fill_count = fill_count.clone();
|
|
let barrier = barrier.clone();
|
|
let fills_per_thread = config.operations_per_thread / 5;
|
|
|
|
tasks.spawn(async move {
|
|
barrier.wait().await;
|
|
|
|
for fill_id in 0..fills_per_thread {
|
|
let fill = create_test_fill(thread_id, fill_id);
|
|
let fill_volume = fill.quantity.value();
|
|
|
|
// Process fill atomically
|
|
match process_fill_concurrent(fill).await {
|
|
Ok(_) => {
|
|
total_fill_volume.fetch_add(fill_volume, Ordering::Relaxed);
|
|
fill_count.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
Err(e) => {
|
|
println!("⚠️ Fill processing error: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for all fill processing to complete
|
|
while let Some(result) = tasks.join_next().await {
|
|
result.expect("Fill processing task should complete successfully");
|
|
}
|
|
|
|
let final_volume = total_fill_volume.load(Ordering::Relaxed);
|
|
let final_count = fill_count.load(Ordering::Relaxed);
|
|
let expected_count = (config.thread_count * config.operations_per_thread / 5) as u64;
|
|
|
|
// Verify fill processing consistency
|
|
assert_eq!(final_count, expected_count);
|
|
assert!(final_volume > 0, "Total fill volume should be positive");
|
|
|
|
// Verify position consistency after fills
|
|
verify_position_consistency_after_fills().await;
|
|
|
|
println!("✅ Concurrent fills processing test passed");
|
|
}
|
|
|
|
/// Test concurrent position updates for accuracy
|
|
async fn test_concurrent_position_updates(config: &ConcurrencyTestConfig) {
|
|
println!("📋 Testing Concurrent Position Updates");
|
|
|
|
let position_map = Arc::new(RwLock::new(HashMap::<Symbol, Position>::new()));
|
|
let update_counter = Arc::new(AtomicU64::new(0));
|
|
let barrier = Arc::new(Barrier::new(config.thread_count));
|
|
|
|
// Initialize positions for test symbols
|
|
{
|
|
let mut positions = position_map.write().await;
|
|
for i in 0..config.concurrent_symbols {
|
|
let symbol = Symbol::new(&format!("TEST{:03}", i)).unwrap();
|
|
let position = Position::new(
|
|
symbol.clone(),
|
|
Side::Buy,
|
|
Quantity::new(1000),
|
|
Price::from_f64(100.0).expect("Valid price"),
|
|
Timestamp::now()
|
|
);
|
|
positions.insert(symbol, position);
|
|
}
|
|
}
|
|
|
|
let mut tasks = JoinSet::new();
|
|
|
|
// Spawn concurrent position update tasks
|
|
for thread_id in 0..config.thread_count {
|
|
let position_map = position_map.clone();
|
|
let update_counter = update_counter.clone();
|
|
let barrier = barrier.clone();
|
|
let updates_per_thread = config.operations_per_thread / 2;
|
|
let symbols_count = config.concurrent_symbols;
|
|
|
|
tasks.spawn(async move {
|
|
barrier.wait().await;
|
|
|
|
for update_id in 0..updates_per_thread {
|
|
let symbol_index = (thread_id + update_id) % symbols_count;
|
|
let symbol = Symbol::new(&format!("TEST{:03}", symbol_index)).unwrap();
|
|
|
|
// Update position atomically
|
|
{
|
|
let mut positions = position_map.write().await;
|
|
if let Some(position) = positions.get_mut(&symbol) {
|
|
// Simulate position update from trade
|
|
let quantity_change = Quantity::new(10);
|
|
position.add_quantity(quantity_change);
|
|
update_counter.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
// Yield to increase concurrency
|
|
tokio::task::yield_now().await;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for all position updates to complete
|
|
while let Some(result) = tasks.join_next().await {
|
|
result.expect("Position update task should complete successfully");
|
|
}
|
|
|
|
let total_updates = update_counter.load(Ordering::Relaxed);
|
|
let expected_updates = (config.thread_count * config.operations_per_thread / 2) as u64;
|
|
|
|
// Verify update count
|
|
assert_eq!(total_updates, expected_updates);
|
|
|
|
// Verify position consistency
|
|
let positions = position_map.read().await;
|
|
for (symbol, position) in &positions {
|
|
assert!(position.quantity.value() >= 1000,
|
|
"Position for {} should have grown from initial 1000", symbol.as_str());
|
|
|
|
// Verify position data integrity
|
|
assert!(position.average_price().to_f64() > 0.0);
|
|
assert!(position.timestamp().as_millis() > 0);
|
|
}
|
|
|
|
println!("✅ Concurrent position updates test passed");
|
|
}
|
|
|
|
/// Test lock-free data structures under high contention
|
|
#[tokio::test]
|
|
async fn test_lock_free_data_structures() {
|
|
println!("🚀 Starting Lock-Free Data Structures Test");
|
|
|
|
let config = ConcurrencyTestConfig::default();
|
|
|
|
// Test atomic operations under contention
|
|
test_atomic_operations_contention(&config).await;
|
|
|
|
// Test lock-free price updates
|
|
test_lock_free_price_updates(&config).await;
|
|
|
|
// Test concurrent order book operations
|
|
test_concurrent_order_book_operations(&config).await;
|
|
|
|
println!("✅ Lock-Free Data Structures Test Completed");
|
|
}
|
|
|
|
/// Test atomic operations under high contention
|
|
async fn test_atomic_operations_contention(config: &ConcurrencyTestConfig) {
|
|
println!("📋 Testing Atomic Operations Under Contention");
|
|
|
|
let atomic_counter = Arc::new(AtomicU64::new(0));
|
|
let atomic_price = Arc::new(AtomicU64::new(100_000_000)); // Price in microunits
|
|
let barrier = Arc::new(Barrier::new(config.thread_count));
|
|
|
|
let mut tasks = JoinSet::new();
|
|
|
|
for thread_id in 0..config.thread_count {
|
|
let atomic_counter = atomic_counter.clone();
|
|
let atomic_price = atomic_price.clone();
|
|
let barrier = barrier.clone();
|
|
let operations = config.operations_per_thread;
|
|
|
|
tasks.spawn(async move {
|
|
barrier.wait().await;
|
|
|
|
for _ in 0..operations {
|
|
// Test different atomic operations
|
|
let _old_counter = atomic_counter.fetch_add(1, Ordering::SeqCst);
|
|
|
|
// Simulate price updates with compare-and-swap
|
|
let current_price = atomic_price.load(Ordering::Acquire);
|
|
let new_price = current_price + 1;
|
|
|
|
// Attempt atomic price update
|
|
let _result = atomic_price.compare_exchange_weak(
|
|
current_price,
|
|
new_price,
|
|
Ordering::Release,
|
|
Ordering::Relaxed
|
|
);
|
|
|
|
// Yield to increase contention
|
|
tokio::task::yield_now().await;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for all atomic operations to complete
|
|
while let Some(result) = tasks.join_next().await {
|
|
result.expect("Atomic operations task should complete successfully");
|
|
}
|
|
|
|
let final_counter = atomic_counter.load(Ordering::SeqCst);
|
|
let final_price = atomic_price.load(Ordering::SeqCst);
|
|
|
|
// Verify atomic operations completed correctly
|
|
let expected_counter = (config.thread_count * config.operations_per_thread) as u64;
|
|
assert_eq!(final_counter, expected_counter);
|
|
assert!(final_price >= 100_000_000); // Price should have increased
|
|
|
|
println!("✅ Atomic operations under contention test passed");
|
|
}
|
|
|
|
/// Test lock-free `price` updates for market data
|
|
async fn test_lock_free_price_updates(config: &ConcurrencyTestConfig) {
|
|
println!("📋 Testing Lock-Free Price Updates");
|
|
|
|
let price_feeds = Arc::new(create_lock_free_price_feeds(config.concurrent_symbols));
|
|
let update_counter = Arc::new(AtomicU64::new(0));
|
|
let barrier = Arc::new(Barrier::new(config.thread_count));
|
|
|
|
let mut tasks = JoinSet::new();
|
|
|
|
// Spawn price update tasks
|
|
for thread_id in 0..config.thread_count {
|
|
let price_feeds = price_feeds.clone();
|
|
let update_counter = update_counter.clone();
|
|
let barrier = barrier.clone();
|
|
let updates_per_thread = config.operations_per_thread;
|
|
let symbols_count = config.concurrent_symbols;
|
|
|
|
tasks.spawn(async move {
|
|
barrier.wait().await;
|
|
|
|
for update_id in 0..updates_per_thread {
|
|
let symbol_index = (thread_id + update_id) % symbols_count;
|
|
let new_price = 100.0 + (update_id as f64 * 0.01);
|
|
|
|
// Update price in lock-free manner
|
|
if update_price_lock_free(&price_feeds, symbol_index, new_price) {
|
|
update_counter.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
// Minimal delay to test rapid updates
|
|
if update_id % 1000 == 0 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for all price updates to complete
|
|
while let Some(result) = tasks.join_next().await {
|
|
result.expect("Price update task should complete successfully");
|
|
}
|
|
|
|
let total_updates = update_counter.load(Ordering::Relaxed);
|
|
|
|
// Verify price updates completed
|
|
assert!(total_updates > 0, "Some price updates should have succeeded");
|
|
|
|
// Verify price feed consistency
|
|
verify_price_feed_consistency(&price_feeds).await;
|
|
|
|
println!("✅ Lock-free price updates test passed");
|
|
}
|
|
|
|
/// Test concurrent order book operations
|
|
async fn test_concurrent_order_book_operations(config: &ConcurrencyTestConfig) {
|
|
println!("📋 Testing Concurrent Order Book Operations");
|
|
|
|
let order_book = Arc::new(create_concurrent_order_book());
|
|
let operation_counter = Arc::new(AtomicU64::new(0));
|
|
let barrier = Arc::new(Barrier::new(config.thread_count));
|
|
|
|
let mut tasks = JoinSet::new();
|
|
|
|
// Spawn order book operation tasks
|
|
for thread_id in 0..config.thread_count {
|
|
let order_book = order_book.clone();
|
|
let operation_counter = operation_counter.clone();
|
|
let barrier = barrier.clone();
|
|
let operations = config.operations_per_thread;
|
|
|
|
tasks.spawn(async move {
|
|
barrier.wait().await;
|
|
|
|
for op_id in 0..operations {
|
|
let operation_type = op_id % 4;
|
|
|
|
match operation_type {
|
|
0 => {
|
|
// Add buy order
|
|
let price = Price::from_f64(99.5 - (op_id as f64 * 0.001).expect("Valid price"));
|
|
let quantity = Quantity::new(100);
|
|
add_order_to_book(&order_book, Side::Buy, price, quantity).await;
|
|
}
|
|
1 => {
|
|
// Add sell order
|
|
let price = Price::from_f64(100.5 + (op_id as f64 * 0.001).expect("Valid price"));
|
|
let quantity = Quantity::new(100);
|
|
add_order_to_book(&order_book, Side::Sell, price, quantity).await;
|
|
}
|
|
2 => {
|
|
// Get best bid/ask
|
|
let _best_prices = get_best_prices(&order_book).await;
|
|
}
|
|
3 => {
|
|
// Cancel random order
|
|
cancel_random_order(&order_book).await;
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
|
|
operation_counter.fetch_add(1, Ordering::Relaxed);
|
|
|
|
if op_id % 100 == 0 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for all order book operations to complete
|
|
while let Some(result) = tasks.join_next().await {
|
|
result.expect("Order book operation task should complete successfully");
|
|
}
|
|
|
|
let total_operations = operation_counter.load(Ordering::Relaxed);
|
|
let expected_operations = (config.thread_count * config.operations_per_thread) as u64;
|
|
|
|
// Verify all operations completed
|
|
assert_eq!(total_operations, expected_operations);
|
|
|
|
// Verify order book integrity
|
|
verify_order_book_integrity(&order_book).await;
|
|
|
|
println!("✅ Concurrent order book operations test passed");
|
|
}
|
|
|
|
/// Property-based test for concurrent operations invariants
|
|
proptest! {
|
|
#[test]
|
|
fn test_concurrent_operations_invariants(
|
|
thread_count in 2..16usize,
|
|
operations_per_thread in 100..1000usize,
|
|
initial_quantity in 1000..10000u64,
|
|
price_range in 50.0..150.0f64
|
|
) {
|
|
tokio_test::block_on(async {
|
|
// Test that concurrent position updates maintain mathematical invariants
|
|
let total_operations = thread_count * operations_per_thread;
|
|
let position = Arc::new(RwLock::new(Position::new(
|
|
Symbol::new("PROPTEST".to_string()),
|
|
Side::Buy,
|
|
Quantity::new(initial_quantity),
|
|
Price::from_f64(price_range).expect("Valid price"),
|
|
Timestamp::now()
|
|
)));
|
|
|
|
let barrier = Arc::new(Barrier::new(thread_count));
|
|
let update_counter = Arc::new(AtomicU64::new(0));
|
|
|
|
let mut tasks = JoinSet::new();
|
|
|
|
for _ in 0..thread_count {
|
|
let position = position.clone();
|
|
let barrier = barrier.clone();
|
|
let update_counter = update_counter.clone();
|
|
|
|
tasks.spawn(async move {
|
|
barrier.wait().await;
|
|
|
|
for _ in 0..operations_per_thread {
|
|
let mut pos = position.write().await;
|
|
pos.add_quantity(Quantity::new(1));
|
|
update_counter.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
});
|
|
}
|
|
|
|
while let Some(result) = tasks.join_next().await {
|
|
result.expect("Task should complete");
|
|
}
|
|
|
|
let final_position = position.read().await;
|
|
let final_quantity = final_position.quantity.value();
|
|
let expected_quantity = initial_quantity + total_operations as u64;
|
|
|
|
prop_assert_eq!(final_quantity, expected_quantity);
|
|
prop_assert_eq!(update_counter.load(Ordering::Relaxed), total_operations as u64);
|
|
});
|
|
}
|
|
}
|
|
|
|
// ===== HELPER FUNCTIONS AND MOCKS =====
|
|
|
|
/// Create a test order with unique characteristics
|
|
fn create_test_order(thread_id: usize, op_id: usize, order_id: u64) -> Order {
|
|
Order::new(
|
|
OrderId::new(order_id),
|
|
Symbol::new(&format!("SYM{:02}", thread_id % 10)).unwrap(),
|
|
Side::Buy,
|
|
OrderType::Limit,
|
|
Quantity::new(100 + op_id as u64),
|
|
Some(Price::from_f64(100.0 + (op_id as f64 * 0.01).expect("Valid price"))),
|
|
TimeInForce::GoodTillCancel
|
|
)
|
|
}
|
|
|
|
/// Create a shared test order for modification testing
|
|
fn create_shared_test_order() -> Order {
|
|
Order::new(
|
|
OrderId::new(999999),
|
|
Symbol::new("SHARED".to_string()),
|
|
Side::Buy,
|
|
OrderType::Limit,
|
|
Quantity::new(1000),
|
|
Some(Price::from_f64(100.0).expect("Valid price")),
|
|
TimeInForce::GoodTillCancel
|
|
)
|
|
}
|
|
|
|
/// Create a test fill
|
|
fn create_test_fill(thread_id: usize, fill_id: usize) -> Fill {
|
|
Fill::new(
|
|
OrderId::new(thread_id as u64 * 1000 + fill_id as u64),
|
|
Symbol::new(&format!("SYM{:02}", thread_id % 10)).unwrap(),
|
|
Side::Buy,
|
|
Quantity::new(50 + fill_id as u64),
|
|
Price::from_f64(100.0 + (fill_id as f64 * 0.001).expect("Valid price")),
|
|
Timestamp::now(),
|
|
Some(format!("FILL_{}_{}_{}", thread_id, fill_id, Uuid::new_v4())),
|
|
None
|
|
)
|
|
}
|
|
|
|
/// Mock function to submit orders concurrently
|
|
async fn submit_order_concurrent(order: Order) -> Result<(), String> {
|
|
// Simulate order processing latency
|
|
tokio::task::yield_now().await;
|
|
|
|
// Simulate occasional failures (5% failure rate)
|
|
if rand::random::<f64>() < 0.05 {
|
|
Err("Simulated order rejection".to_string())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Mock function to process fills concurrently
|
|
async fn process_fill_concurrent(fill: Fill) -> Result<(), String> {
|
|
// Simulate fill processing
|
|
tokio::task::yield_now().await;
|
|
|
|
// Validate fill data
|
|
if fill.quantity.value() == 0 {
|
|
Err("Invalid fill quantity".to_string())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Verify order data integrity after concurrent operations
|
|
async fn verify_order_data_integrity() {
|
|
// Mock verification - in real implementation would check data consistency
|
|
tokio::task::yield_now().await;
|
|
}
|
|
|
|
/// Verify position consistency after fills
|
|
async fn verify_position_consistency_after_fills() {
|
|
// Mock verification - in real implementation would check position calculations
|
|
tokio::task::yield_now().await;
|
|
}
|
|
|
|
/// Create lock-free `price` feeds for testing
|
|
fn create_lock_free_price_feeds(symbol_count: usize) -> Vec<AtomicU64> {
|
|
(0..symbol_count)
|
|
.map(|_| AtomicU64::new(100_000_000)) // Initial price: $100
|
|
.collect()
|
|
}
|
|
|
|
/// Update `price` in lock-free manner
|
|
fn update_price_lock_free(price_feeds: &[AtomicU64], symbol_index: usize, new_price: f64) -> bool {
|
|
if symbol_index >= price_feeds.len() {
|
|
return false;
|
|
}
|
|
|
|
let price_microunits = (new_price * 1_000_000.0) as u64;
|
|
price_feeds[symbol_index].store(price_microunits, Ordering::Release);
|
|
true
|
|
}
|
|
|
|
/// Verify `price` feed consistency
|
|
async fn verify_price_feed_consistency(price_feeds: &[AtomicU64]) {
|
|
for (i, price_feed) in price_feeds.into_iter().enumerate() {
|
|
let price = price_feed.load(Ordering::Acquire);
|
|
assert!(price > 0, "Price feed {} should have positive price", i);
|
|
assert!(price < 1_000_000_000, "Price feed {} should have reasonable price", i);
|
|
}
|
|
}
|
|
|
|
/// Create concurrent order book for testing
|
|
fn create_concurrent_order_book() -> ConcurrentOrderBook {
|
|
ConcurrentOrderBook::new()
|
|
}
|
|
|
|
/// Mock concurrent order book
|
|
#[derive(Debug)]
|
|
struct ConcurrentOrderBook {
|
|
bid_count: AtomicU64,
|
|
ask_count: AtomicU64,
|
|
operation_count: AtomicU64,
|
|
}
|
|
|
|
impl ConcurrentOrderBook {
|
|
fn new() -> Self {
|
|
Self {
|
|
bid_count: AtomicU64::new(0),
|
|
ask_count: AtomicU64::new(0),
|
|
operation_count: AtomicU64::new(0),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Add order to concurrent order book
|
|
async fn add_order_to_book(
|
|
book: &ConcurrentOrderBook,
|
|
side: Side,
|
|
_price: Price,
|
|
_quantity: Quantity,
|
|
) {
|
|
match side {
|
|
Side::Buy => {
|
|
book.bid_count.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
Side::Sell => {
|
|
book.ask_count.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
}
|
|
book.operation_count.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
/// Get best prices from order book
|
|
async fn get_best_prices(book: &ConcurrentOrderBook) -> (Option<Price>, Option<Price>) {
|
|
book.operation_count.fetch_add(1, Ordering::Relaxed);
|
|
(Some(Price::from_f64(99.5).expect("Valid price")), Some(Price::from_f64(100.5).expect("Valid price")))
|
|
}
|
|
|
|
/// Cancel random order from book
|
|
async fn cancel_random_order(book: &ConcurrentOrderBook) {
|
|
book.operation_count.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
/// Verify order book integrity
|
|
async fn verify_order_book_integrity(book: &ConcurrentOrderBook) {
|
|
let bid_count = book.bid_count.load(Ordering::Relaxed);
|
|
let ask_count = book.ask_count.load(Ordering::Relaxed);
|
|
let operation_count = book.operation_count.load(Ordering::Relaxed);
|
|
|
|
assert!(bid_count >= 0);
|
|
assert!(ask_count >= 0);
|
|
assert!(operation_count > 0);
|
|
|
|
println!("📊 Order book stats - Bids: {}, Asks: {}, Total ops: {}",
|
|
bid_count, ask_count, operation_count);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod performance_benchmarks {
|
|
use super::*;
|
|
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId};
|
|
|
|
/// Benchmark concurrent order processing performance
|
|
pub fn bench_concurrent_order_processing(c: &mut Criterion) {
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
|
|
let mut group = c.benchmark_group("concurrent_order_processing");
|
|
|
|
for thread_count in &[1, 2, 4, 8, 16] {
|
|
group.bench_with_input(
|
|
BenchmarkId::new("threads", thread_count),
|
|
thread_count,
|
|
|b, &thread_count| {
|
|
b.to_async(&rt).iter(|| async {
|
|
let config = ConcurrencyTestConfig {
|
|
thread_count,
|
|
operations_per_thread: 1000,
|
|
..Default::default()
|
|
};
|
|
test_concurrent_order_submissions(&config).await;
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
/// Benchmark atomic operations performance
|
|
pub fn bench_atomic_operations(c: &mut Criterion) {
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
|
|
c.bench_function("atomic_price_updates", |b| {
|
|
b.to_async(&rt).iter(|| async {
|
|
let atomic_price = Arc::new(AtomicU64::new(100_000_000));
|
|
|
|
for _ in 0..10000 {
|
|
let current = atomic_price.load(Ordering::Acquire);
|
|
let _result = atomic_price.compare_exchange_weak(
|
|
current,
|
|
current + 1,
|
|
Ordering::Release,
|
|
Ordering::Relaxed,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
criterion_group!(benches, bench_concurrent_order_processing, bench_atomic_operations);
|
|
criterion_main!(benches);
|
|
} |