**Progress: 1,178 → 57 test errors (95% reduction)** ## Status Summary - ✅ Production code: Compiles cleanly (0 errors) - ⚠️ Test code: 57 errors remain (massive improvement) - ⚙️ All services build successfully - 📊 Warning count: 253 (target: <20) - AGENTS WILL FIX ## Remaining Test Errors (57 total) ### Primary Issues: 1. 23× E0308 mismatched types 2. 17× E0433 undeclared Decimal 3. 15× E0433 compliance module not found 4. 6× E0624 private method access 5. Various import and type issues ## Next Phase: Wave 33-2 Launch 10+ parallel agents to: - Fix remaining 57 test compilation errors - Reduce 253 warnings to <20 - Achieve 95% test coverage - Ensure all tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
958 lines
33 KiB
Rust
958 lines
33 KiB
Rust
//! # Databento Enhanced Parser - Production Binary Protocol Processing
|
|
//!
|
|
//! High-level wrapper around the core DBN parser with additional production features:
|
|
//! - Schema validation and version compatibility
|
|
//! - Batch processing optimizations
|
|
//! - Symbol mapping and instrument resolution
|
|
//! - Performance monitoring and alerting
|
|
//! - Error recovery and graceful degradation
|
|
//!
|
|
//! ## Architecture
|
|
//!
|
|
//! ```text
|
|
//! ┌─────────────────────────────────────────────────────────────────────────────┐
|
|
//! │ Enhanced Parser Pipeline │
|
|
//! ├─────────────────────────────────────────────────────────────────────────────┤
|
|
//! │ Raw DBN Data → Schema Validation → Symbol Resolution → Parse Batch │
|
|
//! │ Performance Monitoring → Error Handling → Event Integration │
|
|
//! │ Metrics Collection → Alert Generation → Graceful Degradation │
|
|
//! └─────────────────────────────────────────────────────────────────────────────┘
|
|
//! ```
|
|
//!
|
|
//! ## Performance Features
|
|
//!
|
|
//! - **Adaptive Batch Sizing**: Dynamic batching based on message volume and latency
|
|
//! - **Schema Caching**: Pre-compiled schema parsers for zero-overhead validation
|
|
//! - **Symbol Prefetching**: Proactive resolution of instrument mappings
|
|
//! - **Memory Pool Management**: Efficient allocation patterns for high-frequency parsing
|
|
|
|
use crate::error::{DataError, Result};
|
|
use crate::providers::databento::dbn_parser::{
|
|
DbnParser, DbnParserMetricsSnapshot, ProcessedMessage,
|
|
};
|
|
use crate::providers::databento::types::{DatabentoSchema, DatabentoSymbol};
|
|
use chrono::{DateTime, Utc};
|
|
use common::{Level2Update, MarketDataEvent, Price, PriceLevel, Quantity};
|
|
use rust_decimal::Decimal;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{HashMap, VecDeque};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Instant;
|
|
use tokio::sync::RwLock;
|
|
use tracing::{debug, error, info, instrument, warn};
|
|
use trading_engine::{events::EventProcessor, timing::HardwareTimestamp};
|
|
|
|
/// Enhanced binary parser with production features
|
|
pub struct BinaryParser {
|
|
/// Core DBN parser
|
|
core_parser: Arc<Mutex<DbnParser>>,
|
|
/// Configuration
|
|
config: ParserConfig,
|
|
/// Symbol mapping cache
|
|
symbol_cache: Arc<RwLock<SymbolCache>>,
|
|
/// Schema information
|
|
schema_info: Arc<RwLock<SchemaInfo>>,
|
|
/// Performance metrics
|
|
metrics: Arc<ParserMetrics>,
|
|
/// Event processor integration
|
|
event_processor: Option<Arc<EventProcessor>>,
|
|
/// Batch processor
|
|
batch_processor: Arc<BatchProcessor>,
|
|
}
|
|
|
|
impl BinaryParser {
|
|
/// Create new enhanced parser
|
|
pub async fn new(config: ParserConfig) -> Result<Self> {
|
|
info!("Initializing enhanced DBN parser");
|
|
|
|
let core_parser = Arc::new(Mutex::new(DbnParser::new()?));
|
|
let symbol_cache = Arc::new(RwLock::new(SymbolCache::new(config.symbol_cache_size)));
|
|
let schema_info = Arc::new(RwLock::new(SchemaInfo::new()));
|
|
let batch_processor = Arc::new(BatchProcessor::new(config.clone()));
|
|
|
|
let parser = Self {
|
|
core_parser,
|
|
config: config.clone(),
|
|
symbol_cache,
|
|
schema_info,
|
|
metrics: Arc::new(ParserMetrics::new()),
|
|
event_processor: None,
|
|
batch_processor,
|
|
};
|
|
|
|
// Initialize schema information
|
|
parser.initialize_schemas().await?;
|
|
|
|
info!("Enhanced DBN parser initialized successfully");
|
|
Ok(parser)
|
|
}
|
|
|
|
/// Set event processor for integration
|
|
pub async fn set_event_processor(&mut self, processor: Arc<EventProcessor>) {
|
|
self.event_processor = Some(processor.clone());
|
|
|
|
if let Ok(mut core_parser) = self.core_parser.try_lock() {
|
|
core_parser.set_event_processor(processor);
|
|
}
|
|
}
|
|
|
|
/// Update symbol mappings
|
|
pub async fn update_symbols(&self, symbols: HashMap<u32, DatabentoSymbol>) -> Result<()> {
|
|
let mut cache = self.symbol_cache.write().await;
|
|
cache.update_symbols(symbols)?;
|
|
|
|
// Update core parser symbol mapping
|
|
if let Ok(core_parser) = self.core_parser.try_lock() {
|
|
let symbol_map: HashMap<u32, String> = cache.get_symbol_map();
|
|
core_parser.update_symbol_map(symbol_map);
|
|
}
|
|
|
|
info!("Updated symbol mappings for {} instruments", cache.size());
|
|
Ok(())
|
|
}
|
|
|
|
/// Update price scaling factors
|
|
pub async fn update_price_scales(&self, scales: HashMap<u32, i32>) -> Result<()> {
|
|
if let Ok(core_parser) = self.core_parser.try_lock() {
|
|
core_parser.update_price_scales(scales.clone());
|
|
}
|
|
|
|
let mut schema_info = self.schema_info.write().await;
|
|
schema_info.update_price_scales(scales);
|
|
|
|
debug!(
|
|
"Updated price scales for {} instruments",
|
|
schema_info.price_scales.len()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Parse binary data with enhanced features
|
|
#[instrument(skip(self, data), level = "trace")]
|
|
pub async fn parse_enhanced(&self, data: &[u8]) -> Result<Vec<MarketDataEvent>> {
|
|
let start_time = HardwareTimestamp::now();
|
|
|
|
// Validate input data
|
|
self.validate_input_data(data)?;
|
|
|
|
// Get optimal batch size based on current performance
|
|
let batch_size = self.batch_processor.get_optimal_batch_size().await;
|
|
|
|
// Process data in chunks if it's large
|
|
let processed_messages = if data.len() > batch_size {
|
|
self.process_large_data(data, batch_size).await?
|
|
} else {
|
|
self.process_single_batch(data).await?
|
|
};
|
|
|
|
// Convert to market data events
|
|
let events = self.convert_to_market_events(processed_messages).await?;
|
|
|
|
// Update performance metrics
|
|
let end_time = HardwareTimestamp::now();
|
|
let latency_ns = end_time.latency_ns(&start_time);
|
|
self.metrics
|
|
.record_parse_operation(events.len(), latency_ns);
|
|
|
|
// Check performance targets
|
|
self.check_performance_targets(latency_ns, events.len())
|
|
.await;
|
|
|
|
debug!("Parsed {} events in {}ns", events.len(), latency_ns);
|
|
Ok(events)
|
|
}
|
|
|
|
/// Process large data sets in optimized batches
|
|
async fn process_large_data(
|
|
&self,
|
|
data: &[u8],
|
|
batch_size: usize,
|
|
) -> Result<Vec<ProcessedMessage>> {
|
|
let mut all_messages = Vec::new();
|
|
let mut offset = 0;
|
|
|
|
while offset < data.len() {
|
|
let chunk_end = (offset + batch_size).min(data.len());
|
|
let chunk = &data[offset..chunk_end];
|
|
|
|
let messages = self.process_single_batch(chunk).await?;
|
|
all_messages.extend(messages);
|
|
|
|
offset = chunk_end;
|
|
|
|
// Yield control periodically to prevent blocking
|
|
if all_messages.len() % 10000 == 0 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
}
|
|
|
|
Ok(all_messages)
|
|
}
|
|
|
|
/// Process single batch of data
|
|
async fn process_single_batch(&self, data: &[u8]) -> Result<Vec<ProcessedMessage>> {
|
|
if let Ok(core_parser) = self.core_parser.try_lock() {
|
|
match core_parser.parse_batch(data) {
|
|
Ok(messages) => {
|
|
self.metrics.record_successful_batch(messages.len());
|
|
Ok(messages)
|
|
},
|
|
Err(e) => {
|
|
self.metrics.record_failed_batch();
|
|
error!("Failed to parse batch: {}", e);
|
|
|
|
// Attempt graceful degradation
|
|
self.attempt_recovery(data).await
|
|
},
|
|
}
|
|
} else {
|
|
Err(DataError::internal("Core parser locked"))
|
|
}
|
|
}
|
|
|
|
/// Attempt to recover from parsing errors
|
|
async fn attempt_recovery(&self, data: &[u8]) -> Result<Vec<ProcessedMessage>> {
|
|
warn!("Attempting graceful recovery from parsing error");
|
|
|
|
// Try to parse individual messages instead of batch
|
|
let mut recovered_messages = Vec::new();
|
|
let mut offset = 0;
|
|
|
|
// Simple recovery: skip corrupted sections and try to find valid messages
|
|
while offset + 16 < data.len() {
|
|
// Minimum header size
|
|
match self.try_parse_single_message(&data[offset..]) {
|
|
Ok(Some((message, consumed))) => {
|
|
recovered_messages.push(message);
|
|
offset += consumed;
|
|
self.metrics.record_recovered_message();
|
|
},
|
|
Ok(None) => {
|
|
offset += 1; // Skip byte and continue
|
|
},
|
|
Err(_) => {
|
|
offset += 1; // Skip byte and continue
|
|
},
|
|
}
|
|
|
|
// Limit recovery attempts to prevent infinite loops
|
|
if recovered_messages.len() > 1000 {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if recovered_messages.is_empty() {
|
|
Err(DataError::InvalidFormat(
|
|
"No recoverable messages found".to_string(),
|
|
))
|
|
} else {
|
|
warn!(
|
|
"Recovered {} messages from corrupted batch",
|
|
recovered_messages.len()
|
|
);
|
|
Ok(recovered_messages)
|
|
}
|
|
}
|
|
|
|
/// Try to parse a single message from data
|
|
fn try_parse_single_message(&self, _data: &[u8]) -> Result<Option<(ProcessedMessage, usize)>> {
|
|
// This would implement single message parsing logic
|
|
// For now, return None as placeholder
|
|
Ok(None)
|
|
}
|
|
|
|
/// Convert processed messages to market data events
|
|
async fn convert_to_market_events(
|
|
&self,
|
|
messages: Vec<ProcessedMessage>,
|
|
) -> Result<Vec<MarketDataEvent>> {
|
|
let mut events = Vec::with_capacity(messages.len());
|
|
let symbol_cache = self.symbol_cache.read().await;
|
|
|
|
for message in messages {
|
|
match message {
|
|
ProcessedMessage::Trade {
|
|
symbol,
|
|
timestamp,
|
|
price,
|
|
size,
|
|
side: _side,
|
|
trade_id,
|
|
conditions,
|
|
} => {
|
|
let resolved_symbol = symbol_cache.resolve_symbol(&symbol).unwrap_or(symbol);
|
|
|
|
events.push(MarketDataEvent::Trade(common::TradeEvent {
|
|
symbol: resolved_symbol.into(),
|
|
price: Decimal::from(price),
|
|
size: Decimal::from(size),
|
|
timestamp: hardware_timestamp_to_chrono(×tamp),
|
|
trade_id: Some(trade_id.unwrap_or_else(|| "UNKNOWN".to_string())),
|
|
exchange: Some("DATABENTO".to_string()),
|
|
conditions,
|
|
sequence: 0,
|
|
}));
|
|
},
|
|
|
|
ProcessedMessage::Quote {
|
|
symbol,
|
|
timestamp,
|
|
bid,
|
|
ask,
|
|
bid_size,
|
|
ask_size,
|
|
exchange,
|
|
} => {
|
|
let resolved_symbol = symbol_cache.resolve_symbol(&symbol).unwrap_or(symbol);
|
|
|
|
events.push(MarketDataEvent::Quote(common::QuoteEvent {
|
|
symbol: resolved_symbol.into(),
|
|
bid: bid.map(Decimal::from),
|
|
ask: ask.map(Decimal::from),
|
|
bid_size: bid_size.map(Decimal::from),
|
|
ask_size: ask_size.map(Decimal::from),
|
|
timestamp: hardware_timestamp_to_chrono(×tamp),
|
|
exchange: Some("DATABENTO".to_string()),
|
|
bid_exchange: exchange.clone(),
|
|
ask_exchange: exchange.clone(),
|
|
conditions: vec![],
|
|
sequence: 0,
|
|
}));
|
|
},
|
|
|
|
ProcessedMessage::OrderBook {
|
|
symbol,
|
|
timestamp,
|
|
price,
|
|
size,
|
|
side,
|
|
action,
|
|
level: _level,
|
|
order_id: _order_id,
|
|
} => {
|
|
let resolved_symbol = symbol_cache.resolve_symbol(&symbol).unwrap_or(symbol);
|
|
|
|
use crate::providers::common::{
|
|
OrderBookSide, PriceLevelChange, PriceLevelChangeType,
|
|
};
|
|
|
|
let change = PriceLevelChange {
|
|
price: Price::from_decimal(Decimal::from(price)),
|
|
quantity: Quantity::from_decimal(Decimal::from(size))
|
|
.unwrap_or(Quantity::zero()),
|
|
change_type: match action.to_string().as_str() {
|
|
"Add" => PriceLevelChangeType::Add,
|
|
"Update" => PriceLevelChangeType::Update,
|
|
"Delete" => PriceLevelChangeType::Delete,
|
|
_ => PriceLevelChangeType::Update,
|
|
},
|
|
side: match side.to_string().as_str() {
|
|
"Bid" => OrderBookSide::Bid,
|
|
"Ask" => OrderBookSide::Ask,
|
|
_ => OrderBookSide::Bid,
|
|
},
|
|
};
|
|
|
|
// Clone change for later use since we need it twice
|
|
let change_side = change.side.clone();
|
|
let change_price = change.price;
|
|
let change_size = change.quantity;
|
|
|
|
let (_bid_changes, _ask_changes) = match change.side {
|
|
OrderBookSide::Bid => (vec![change], vec![]),
|
|
OrderBookSide::Ask => (vec![], vec![change]),
|
|
};
|
|
|
|
// Convert the change to level2 format
|
|
let mut bids = Vec::new();
|
|
let mut asks = Vec::new();
|
|
|
|
match change_side {
|
|
OrderBookSide::Bid => {
|
|
bids.push(PriceLevel {
|
|
price: change_price.into(),
|
|
size: change_size.into(),
|
|
});
|
|
},
|
|
OrderBookSide::Ask => {
|
|
asks.push(PriceLevel {
|
|
price: change_price.into(),
|
|
size: change_size.into(),
|
|
});
|
|
},
|
|
}
|
|
|
|
events.push(MarketDataEvent::Level2(Level2Update {
|
|
symbol: resolved_symbol.into(),
|
|
bids,
|
|
asks,
|
|
timestamp: hardware_timestamp_to_chrono(×tamp),
|
|
}));
|
|
},
|
|
|
|
ProcessedMessage::Ohlcv {
|
|
symbol,
|
|
timestamp,
|
|
open,
|
|
high,
|
|
low,
|
|
close,
|
|
volume,
|
|
} => {
|
|
let resolved_symbol = symbol_cache.resolve_symbol(&symbol).unwrap_or(symbol);
|
|
|
|
let ts = hardware_timestamp_to_chrono(×tamp);
|
|
events.push(MarketDataEvent::Bar(common::BarEvent {
|
|
symbol: resolved_symbol.into(),
|
|
open: Decimal::from(open),
|
|
high: Decimal::from(high),
|
|
low: Decimal::from(low),
|
|
close: Decimal::from(close),
|
|
volume: Decimal::from(volume),
|
|
vwap: None,
|
|
start_timestamp: ts,
|
|
end_timestamp: ts,
|
|
timeframe: "1m".to_string(), // Default timeframe
|
|
}));
|
|
},
|
|
|
|
ProcessedMessage::Status { timestamp, message } => {
|
|
debug!(
|
|
"Status message at {}: {}",
|
|
hardware_timestamp_to_chrono(×tamp),
|
|
message
|
|
);
|
|
// Status messages are typically not converted to market events
|
|
},
|
|
}
|
|
}
|
|
|
|
// Sort events by timestamp for proper chronological order
|
|
events.sort_by_key(|event| event.timestamp());
|
|
|
|
Ok(events)
|
|
}
|
|
|
|
/// Validate input data format and size
|
|
fn validate_input_data(&self, data: &[u8]) -> Result<()> {
|
|
if data.is_empty() {
|
|
return Err(DataError::InvalidFormat("Empty input data".to_string()));
|
|
}
|
|
|
|
if data.len() > self.config.max_batch_size {
|
|
return Err(DataError::InvalidFormat(format!(
|
|
"Input data too large: {} bytes (max: {})",
|
|
data.len(),
|
|
self.config.max_batch_size
|
|
)));
|
|
}
|
|
|
|
// Basic DBN format validation
|
|
if data.len() < 16 {
|
|
return Err(DataError::InvalidFormat(
|
|
"Data too short for DBN header".to_string(),
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if performance meets targets and alert if not
|
|
async fn check_performance_targets(&self, latency_ns: u64, event_count: usize) {
|
|
let per_event_latency = if event_count > 0 {
|
|
latency_ns / event_count as u64
|
|
} else {
|
|
latency_ns
|
|
};
|
|
|
|
// Check latency targets
|
|
if per_event_latency > self.config.max_latency_per_event_ns {
|
|
warn!(
|
|
"Parser performance degraded: {}ns per event (target: {}ns)",
|
|
per_event_latency, self.config.max_latency_per_event_ns
|
|
);
|
|
|
|
self.metrics.record_performance_violation();
|
|
|
|
// Trigger adaptive optimizations
|
|
self.batch_processor.adjust_for_high_latency().await;
|
|
}
|
|
|
|
// Check throughput targets
|
|
let throughput = (event_count as f64 / latency_ns as f64) * 1_000_000_000.0; // events per second
|
|
if throughput < self.config.min_throughput_events_per_sec {
|
|
warn!(
|
|
"Parser throughput below target: {:.0} events/sec (target: {})",
|
|
throughput, self.config.min_throughput_events_per_sec
|
|
);
|
|
|
|
self.batch_processor.adjust_for_low_throughput().await;
|
|
}
|
|
}
|
|
|
|
/// Initialize schema information
|
|
async fn initialize_schemas(&self) -> Result<()> {
|
|
let mut schema_info = self.schema_info.write().await;
|
|
|
|
// Initialize supported schemas
|
|
schema_info.add_schema(
|
|
DatabentoSchema::Trades,
|
|
SchemaMetadata {
|
|
version: 1,
|
|
message_size: 32,
|
|
supports_batching: true,
|
|
typical_frequency: 1000.0, // 1000 messages per second
|
|
},
|
|
);
|
|
|
|
schema_info.add_schema(
|
|
DatabentoSchema::Tbbo,
|
|
SchemaMetadata {
|
|
version: 1,
|
|
message_size: 48,
|
|
supports_batching: true,
|
|
typical_frequency: 500.0,
|
|
},
|
|
);
|
|
|
|
schema_info.add_schema(
|
|
DatabentoSchema::Mbo,
|
|
SchemaMetadata {
|
|
version: 1,
|
|
message_size: 48,
|
|
supports_batching: true,
|
|
typical_frequency: 2000.0,
|
|
},
|
|
);
|
|
|
|
schema_info.add_schema(
|
|
DatabentoSchema::Ohlcv1M,
|
|
SchemaMetadata {
|
|
version: 1,
|
|
message_size: 56,
|
|
supports_batching: true,
|
|
typical_frequency: 1.0, // 1 per minute
|
|
},
|
|
);
|
|
|
|
debug!(
|
|
"Initialized {} schema definitions",
|
|
schema_info.schemas.len()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Get parser metrics
|
|
pub async fn get_metrics(&self) -> ParserMetricsSnapshot {
|
|
self.metrics.get_snapshot().await
|
|
}
|
|
|
|
/// Get core parser metrics
|
|
pub async fn get_core_metrics(&self) -> Result<DbnParserMetricsSnapshot> {
|
|
if let Ok(core_parser) = self.core_parser.try_lock() {
|
|
Ok(core_parser.get_metrics())
|
|
} else {
|
|
Err(DataError::internal("Core parser locked"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parser configuration
|
|
#[derive(Debug, Clone)]
|
|
pub struct ParserConfig {
|
|
/// Maximum batch size in bytes
|
|
pub max_batch_size: usize,
|
|
/// Maximum latency per event in nanoseconds
|
|
pub max_latency_per_event_ns: u64,
|
|
/// Minimum throughput in events per second
|
|
pub min_throughput_events_per_sec: f64,
|
|
/// Symbol cache size
|
|
pub symbol_cache_size: usize,
|
|
/// Enable graceful recovery
|
|
pub enable_recovery: bool,
|
|
/// Enable adaptive batch sizing
|
|
pub enable_adaptive_batching: bool,
|
|
}
|
|
|
|
impl ParserConfig {
|
|
pub fn production() -> Self {
|
|
Self {
|
|
max_batch_size: 1024 * 1024, // 1MB
|
|
max_latency_per_event_ns: 1_000, // 1μs per event
|
|
min_throughput_events_per_sec: 100_000.0, // 100k events/sec
|
|
symbol_cache_size: 10_000,
|
|
enable_recovery: true,
|
|
enable_adaptive_batching: true,
|
|
}
|
|
}
|
|
|
|
pub fn testing() -> Self {
|
|
Self {
|
|
max_batch_size: 64 * 1024, // 64KB
|
|
max_latency_per_event_ns: 10_000, // 10μs per event
|
|
min_throughput_events_per_sec: 1_000.0, // 1k events/sec
|
|
symbol_cache_size: 1_000,
|
|
enable_recovery: false,
|
|
enable_adaptive_batching: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Symbol cache for efficient resolution
|
|
struct SymbolCache {
|
|
symbols: HashMap<u32, DatabentoSymbol>,
|
|
reverse_lookup: HashMap<String, u32>,
|
|
max_size: usize,
|
|
access_count: HashMap<u32, u64>,
|
|
}
|
|
|
|
impl SymbolCache {
|
|
fn new(max_size: usize) -> Self {
|
|
Self {
|
|
symbols: HashMap::new(),
|
|
reverse_lookup: HashMap::new(),
|
|
max_size,
|
|
access_count: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn update_symbols(&mut self, symbols: HashMap<u32, DatabentoSymbol>) -> Result<()> {
|
|
// Clear existing mappings
|
|
self.symbols.clear();
|
|
self.reverse_lookup.clear();
|
|
self.access_count.clear();
|
|
|
|
// Add new symbols with size limit
|
|
let mut added = 0;
|
|
for (id, symbol) in symbols {
|
|
if added >= self.max_size {
|
|
break;
|
|
}
|
|
|
|
self.reverse_lookup.insert(symbol.normalized.clone(), id);
|
|
self.symbols.insert(id, symbol);
|
|
self.access_count.insert(id, 0);
|
|
added += 1;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn resolve_symbol(&self, symbol: &str) -> Option<String> {
|
|
if let Some(&id) = self.reverse_lookup.get(symbol) {
|
|
if let Some(databento_symbol) = self.symbols.get(&id) {
|
|
// Update access count (in a real implementation, this would be atomic)
|
|
return Some(databento_symbol.normalized.clone());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn get_symbol_map(&self) -> HashMap<u32, String> {
|
|
self.symbols
|
|
.iter()
|
|
.map(|(&id, symbol)| (id, symbol.normalized.clone()))
|
|
.collect()
|
|
}
|
|
|
|
fn size(&self) -> usize {
|
|
self.symbols.len()
|
|
}
|
|
}
|
|
|
|
/// Schema information and metadata
|
|
struct SchemaInfo {
|
|
schemas: HashMap<DatabentoSchema, SchemaMetadata>,
|
|
price_scales: HashMap<u32, i32>,
|
|
}
|
|
|
|
impl SchemaInfo {
|
|
fn new() -> Self {
|
|
Self {
|
|
schemas: HashMap::new(),
|
|
price_scales: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn add_schema(&mut self, schema: DatabentoSchema, metadata: SchemaMetadata) {
|
|
self.schemas.insert(schema, metadata);
|
|
}
|
|
|
|
fn update_price_scales(&mut self, scales: HashMap<u32, i32>) {
|
|
self.price_scales.extend(scales);
|
|
}
|
|
}
|
|
|
|
/// Schema metadata
|
|
#[derive(Debug, Clone)]
|
|
struct SchemaMetadata {
|
|
version: u32,
|
|
message_size: usize,
|
|
supports_batching: bool,
|
|
typical_frequency: f64, // messages per second
|
|
}
|
|
|
|
/// Batch processor for adaptive optimization
|
|
struct BatchProcessor {
|
|
config: ParserConfig,
|
|
current_batch_size: Arc<std::sync::atomic::AtomicUsize>,
|
|
performance_history: Arc<Mutex<VecDeque<PerformanceSample>>>,
|
|
}
|
|
|
|
impl BatchProcessor {
|
|
fn new(config: ParserConfig) -> Self {
|
|
Self {
|
|
config,
|
|
current_batch_size: Arc::new(std::sync::atomic::AtomicUsize::new(8192)), // 8KB initial
|
|
performance_history: Arc::new(Mutex::new(VecDeque::with_capacity(100))),
|
|
}
|
|
}
|
|
|
|
async fn get_optimal_batch_size(&self) -> usize {
|
|
self.current_batch_size
|
|
.load(std::sync::atomic::Ordering::Relaxed)
|
|
}
|
|
|
|
async fn adjust_for_high_latency(&self) {
|
|
// Reduce batch size to improve latency
|
|
let current = self
|
|
.current_batch_size
|
|
.load(std::sync::atomic::Ordering::Relaxed);
|
|
let new_size = (current / 2).max(1024); // Minimum 1KB
|
|
self.current_batch_size
|
|
.store(new_size, std::sync::atomic::Ordering::Relaxed);
|
|
|
|
debug!(
|
|
"Reduced batch size to {} bytes due to high latency",
|
|
new_size
|
|
);
|
|
}
|
|
|
|
async fn adjust_for_low_throughput(&self) {
|
|
// Increase batch size to improve throughput
|
|
let current = self
|
|
.current_batch_size
|
|
.load(std::sync::atomic::Ordering::Relaxed);
|
|
let new_size = (current * 2).min(self.config.max_batch_size);
|
|
self.current_batch_size
|
|
.store(new_size, std::sync::atomic::Ordering::Relaxed);
|
|
|
|
debug!(
|
|
"Increased batch size to {} bytes due to low throughput",
|
|
new_size
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Performance sample for adaptive optimization
|
|
#[derive(Debug, Clone)]
|
|
struct PerformanceSample {
|
|
timestamp: Instant,
|
|
latency_ns: u64,
|
|
throughput: f64,
|
|
batch_size: usize,
|
|
}
|
|
|
|
/// Enhanced parser metrics
|
|
pub struct ParserMetrics {
|
|
// Operation metrics
|
|
total_operations: std::sync::atomic::AtomicU64,
|
|
successful_batches: std::sync::atomic::AtomicU64,
|
|
failed_batches: std::sync::atomic::AtomicU64,
|
|
recovered_messages: std::sync::atomic::AtomicU64,
|
|
|
|
// Performance metrics
|
|
total_latency_ns: std::sync::atomic::AtomicU64,
|
|
total_events_processed: std::sync::atomic::AtomicU64,
|
|
performance_violations: std::sync::atomic::AtomicU64,
|
|
|
|
start_time: Instant,
|
|
}
|
|
|
|
impl ParserMetrics {
|
|
fn new() -> Self {
|
|
Self {
|
|
total_operations: std::sync::atomic::AtomicU64::new(0),
|
|
successful_batches: std::sync::atomic::AtomicU64::new(0),
|
|
failed_batches: std::sync::atomic::AtomicU64::new(0),
|
|
recovered_messages: std::sync::atomic::AtomicU64::new(0),
|
|
total_latency_ns: std::sync::atomic::AtomicU64::new(0),
|
|
total_events_processed: std::sync::atomic::AtomicU64::new(0),
|
|
performance_violations: std::sync::atomic::AtomicU64::new(0),
|
|
start_time: Instant::now(),
|
|
}
|
|
}
|
|
|
|
fn record_parse_operation(&self, event_count: usize, latency_ns: u64) {
|
|
self.total_operations
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
self.total_events_processed
|
|
.fetch_add(event_count as u64, std::sync::atomic::Ordering::Relaxed);
|
|
self.total_latency_ns
|
|
.fetch_add(latency_ns, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
|
|
fn record_successful_batch(&self, _event_count: usize) {
|
|
self.successful_batches
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
|
|
fn record_failed_batch(&self) {
|
|
self.failed_batches
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
|
|
fn record_recovered_message(&self) {
|
|
self.recovered_messages
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
|
|
fn record_performance_violation(&self) {
|
|
self.performance_violations
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
|
|
async fn get_snapshot(&self) -> ParserMetricsSnapshot {
|
|
let uptime = self.start_time.elapsed();
|
|
let operations = self
|
|
.total_operations
|
|
.load(std::sync::atomic::Ordering::Relaxed);
|
|
let events = self
|
|
.total_events_processed
|
|
.load(std::sync::atomic::Ordering::Relaxed);
|
|
let latency = self
|
|
.total_latency_ns
|
|
.load(std::sync::atomic::Ordering::Relaxed);
|
|
|
|
ParserMetricsSnapshot {
|
|
total_operations: operations,
|
|
successful_batches: self
|
|
.successful_batches
|
|
.load(std::sync::atomic::Ordering::Relaxed),
|
|
failed_batches: self
|
|
.failed_batches
|
|
.load(std::sync::atomic::Ordering::Relaxed),
|
|
recovered_messages: self
|
|
.recovered_messages
|
|
.load(std::sync::atomic::Ordering::Relaxed),
|
|
total_events_processed: events,
|
|
performance_violations: self
|
|
.performance_violations
|
|
.load(std::sync::atomic::Ordering::Relaxed),
|
|
avg_latency_ns: if operations > 0 {
|
|
latency / operations
|
|
} else {
|
|
0
|
|
},
|
|
events_per_second: if uptime.as_secs() > 0 {
|
|
events / uptime.as_secs()
|
|
} else {
|
|
0
|
|
},
|
|
uptime_seconds: uptime.as_secs(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parser metrics snapshot
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ParserMetricsSnapshot {
|
|
pub total_operations: u64,
|
|
pub successful_batches: u64,
|
|
pub failed_batches: u64,
|
|
pub recovered_messages: u64,
|
|
pub total_events_processed: u64,
|
|
pub performance_violations: u64,
|
|
pub avg_latency_ns: u64,
|
|
pub events_per_second: u64,
|
|
pub uptime_seconds: u64,
|
|
}
|
|
|
|
/// Helper function to convert HardwareTimestamp to chrono DateTime
|
|
fn hardware_timestamp_to_chrono(timestamp: &HardwareTimestamp) -> DateTime<Utc> {
|
|
let nanos = timestamp.as_nanos();
|
|
let secs = nanos / 1_000_000_000;
|
|
let nsecs = (nanos % 1_000_000_000) as u32;
|
|
DateTime::from_timestamp(secs as i64, nsecs).unwrap_or_else(|| Utc::now())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::providers::databento::types::DatabentoSType;
|
|
use tokio::test;
|
|
|
|
#[test]
|
|
async fn test_parser_creation() {
|
|
let config = ParserConfig::testing();
|
|
let parser = BinaryParser::new(config).await;
|
|
assert!(parser.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
async fn test_symbol_cache() {
|
|
let mut cache = SymbolCache::new(1000);
|
|
|
|
let mut symbols = HashMap::new();
|
|
symbols.insert(
|
|
1,
|
|
DatabentoSymbol {
|
|
raw: "AAPL".to_string(),
|
|
normalized: "AAPL".to_string(),
|
|
instrument_id: 1,
|
|
stype: DatabentoSType::RawSymbol,
|
|
},
|
|
);
|
|
|
|
assert!(cache.update_symbols(symbols).is_ok());
|
|
assert_eq!(cache.size(), 1);
|
|
assert_eq!(cache.resolve_symbol("AAPL"), Some("AAPL".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
async fn test_batch_processor() {
|
|
let config = ParserConfig::testing();
|
|
let processor = BatchProcessor::new(config);
|
|
|
|
let initial_size = processor.get_optimal_batch_size().await;
|
|
|
|
processor.adjust_for_high_latency().await;
|
|
let reduced_size = processor.get_optimal_batch_size().await;
|
|
assert!(reduced_size < initial_size);
|
|
|
|
processor.adjust_for_low_throughput().await;
|
|
let increased_size = processor.get_optimal_batch_size().await;
|
|
assert!(increased_size > reduced_size);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_parser_metrics() {
|
|
let metrics = ParserMetrics::new();
|
|
|
|
metrics.record_parse_operation(100, 5000); // 100 events, 5μs
|
|
metrics.record_successful_batch(100);
|
|
|
|
let snapshot = metrics.get_snapshot().await;
|
|
assert_eq!(snapshot.total_operations, 1);
|
|
assert_eq!(snapshot.successful_batches, 1);
|
|
assert_eq!(snapshot.total_events_processed, 100);
|
|
assert_eq!(snapshot.avg_latency_ns, 5000);
|
|
}
|
|
|
|
#[test]
|
|
async fn test_input_validation() {
|
|
let config = ParserConfig::testing();
|
|
let parser = BinaryParser::new(config).await.unwrap();
|
|
|
|
// Empty data should fail
|
|
assert!(parser.validate_input_data(&[]).is_err());
|
|
|
|
// Too short data should fail
|
|
assert!(parser.validate_input_data(&[1, 2, 3]).is_err());
|
|
|
|
// Minimum valid size should pass
|
|
let valid_data = vec![0u8; 16];
|
|
assert!(parser.validate_input_data(&valid_data).is_ok());
|
|
}
|
|
}
|