//! Online feature serving with low-latency retrieval //! //! This module provides real-time feature serving capabilities with //! sub-100ms latency targets for production ML systems. use chrono::{DateTime, Utc}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; use crate::store::{FeatureStore, FeatureValue}; use crate::{FeatureStoreError, Result}; /// Configuration for online serving #[derive(Debug, Clone)] pub struct ServingConfig { /// Port to bind the serving endpoint pub port: u16, /// Maximum allowed latency for requests pub max_latency: Duration, /// Enable performance metrics collection pub enable_metrics: bool, /// Maximum batch size for bulk requests pub batch_size_limit: usize, } impl Default for ServingConfig { fn default() -> Self { Self { port: 8080, max_latency: Duration::from_millis(100), enable_metrics: true, batch_size_limit: 1000, } } } /// Request for feature retrieval #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureRequest { /// Entity ID to retrieve features for pub entity_id: String, /// List of feature names to retrieve pub feature_names: Vec, /// Optional timestamp for point-in-time queries pub timestamp: Option>, } /// Response containing retrieved features #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureResponse { /// Retrieved features indexed by feature name pub features: HashMap, /// Timestamp of the response pub timestamp: DateTime, /// Latency in milliseconds pub latency_ms: u64, /// Any warnings or errors encountered pub warnings: Vec, } /// Batch request for multiple entities #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BatchFeatureRequest { /// Entity IDs to retrieve features for pub entity_ids: Vec, /// List of feature names to retrieve pub feature_names: Vec, /// Optional timestamp for point-in-time queries pub timestamp: Option>, } /// Batch response containing features for multiple entities #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BatchFeatureResponse { /// Features indexed by entity ID, then by feature name pub entity_features: HashMap>, /// Timestamp of the response pub timestamp: DateTime, /// Latency in milliseconds pub latency_ms: u64, /// Any warnings or errors encountered pub warnings: Vec, } /// Serving metrics for monitoring performance #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ServingMetrics { /// Total number of requests served pub total_requests: u64, /// Total number of batch requests served pub batch_requests: u64, /// Average latency in milliseconds pub avg_latency_ms: f64, /// 95th percentile latency pub p95_latency_ms: f64, /// 99th percentile latency pub p99_latency_ms: f64, /// Number of cache hits pub cache_hits: u64, /// Number of cache misses pub cache_misses: u64, /// Number of errors pub error_count: u64, /// Timestamp of last metric update pub last_updated: DateTime, } /// Latency tracker for calculating percentiles #[derive(Debug)] struct LatencyTracker { samples: Vec, max_samples: usize, } impl LatencyTracker { fn new(max_samples: usize) -> Self { Self { samples: Vec::with_capacity(max_samples), max_samples, } } fn record(&mut self, latency: Duration) { self.samples.push(latency); if self.samples.len() > self.max_samples { self.samples.remove(0); } } fn percentile(&self, p: f64) -> Duration { if self.samples.is_empty() { return Duration::from_millis(0); } let mut sorted = self.samples.clone(); sorted.sort(); let index = ((p / 100.0) * (sorted.len() as f64 - 1.0)).round() as usize; sorted .get(index) .copied() .unwrap_or(Duration::from_millis(0)) } fn average(&self) -> Duration { if self.samples.is_empty() { return Duration::from_millis(0); } let sum: Duration = self.samples.iter().sum(); sum / self.samples.len() as u32 } } /// Drift detection report #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DriftReport { /// Feature name being monitored pub feature_name: String, /// Whether drift was detected pub drift_detected: bool, /// Drift score (0.0 = no drift, 1.0 = maximum drift) pub drift_score: f64, /// Threshold used for detection pub threshold: f64, /// List of drift alerts pub alerts: Vec, /// Timestamp of the report pub timestamp: DateTime, } /// Online serving server for real-time feature retrieval pub struct OnlineServer { store: Arc>, config: ServingConfig, metrics: Arc>, latency_tracker: Arc>, } impl OnlineServer { /// Create a new online serving server pub async fn new(store: FeatureStore, config: ServingConfig) -> Result { Ok(Self { store: Arc::new(RwLock::new(store)), config, metrics: Arc::new(Mutex::new(ServingMetrics::default())), latency_tracker: Arc::new(Mutex::new(LatencyTracker::new(10000))), // Keep last 10k samples }) } /// Serve features for a single entity pub async fn serve_features(&self, request: FeatureRequest) -> Result { let start_time = Instant::now(); let mut warnings = Vec::new(); // Validate request if request.feature_names.is_empty() { return Err(FeatureStoreError::Storage( "No feature names provided".to_string(), )); } if request.entity_id.is_empty() { return Err(FeatureStoreError::Storage( "No entity ID provided".to_string(), )); } let mut store = self.store.write().await; let mut features = HashMap::new(); // Retrieve each requested feature for feature_name in &request.feature_names { let feature_result = if let Some(timestamp) = request.timestamp { store .get_feature_at_time(feature_name, &request.entity_id, timestamp) .await } else { store.get_feature(feature_name, &request.entity_id).await }; match feature_result { Ok(value) => { features.insert(feature_name.clone(), value); } Err(FeatureStoreError::NotFound(msg)) => { warnings.push(format!("Feature not found: {msg}")); features.insert(feature_name.clone(), FeatureValue::Null); } Err(e) => return Err(e), } } let latency = start_time.elapsed(); let latency_ms = latency.as_millis() as u64; // Check latency threshold if latency > self.config.max_latency { warnings.push(format!( "Request latency {}ms exceeded threshold {}ms", latency_ms, self.config.max_latency.as_millis() )); } // Update metrics if self.config.enable_metrics { self.update_metrics(latency, false, warnings.is_empty()) .await; } Ok(FeatureResponse { features, timestamp: Utc::now(), latency_ms, warnings, }) } /// Serve features for multiple entities (batch request) pub async fn serve_features_batch( &self, request: BatchFeatureRequest, ) -> Result { let start_time = Instant::now(); let mut warnings = Vec::new(); // Validate request if request.entity_ids.is_empty() { return Err(FeatureStoreError::Storage( "No entity IDs provided".to_string(), )); } if request.feature_names.is_empty() { return Err(FeatureStoreError::Storage( "No feature names provided".to_string(), )); } if request.entity_ids.len() > self.config.batch_size_limit { return Err(FeatureStoreError::Storage(format!( "Batch size {} exceeds limit {}", request.entity_ids.len(), self.config.batch_size_limit ))); } let mut store = self.store.write().await; let mut entity_features = HashMap::new(); // Process each feature for all entities for feature_name in &request.feature_names { let batch_result = if request.timestamp.is_some() { // For temporal queries, we need to fetch individually let mut feature_map = HashMap::new(); for entity_id in &request.entity_ids { let feature_result = store .get_feature_at_time(feature_name, entity_id, request.timestamp.unwrap()) .await; match feature_result { Ok(value) => { feature_map.insert(entity_id.clone(), value); } Err(FeatureStoreError::NotFound(_)) => { feature_map.insert(entity_id.clone(), FeatureValue::Null); } Err(e) => return Err(e), } } Ok(crate::store::FeatureBatch { features: feature_map, timestamp: Utc::now(), feature_name: feature_name.clone(), }) } else { store .get_features_batch(feature_name, &request.entity_ids) .await }; match batch_result { Ok(batch) => { for entity_id in &request.entity_ids { let entity_feature_map = entity_features .entry(entity_id.clone()) .or_insert_with(HashMap::new); let value = batch .features .get(entity_id) .cloned() .unwrap_or(FeatureValue::Null); entity_feature_map.insert(feature_name.clone(), value); } } Err(FeatureStoreError::NotFound(msg)) => { warnings.push(format!("Feature batch not found: {msg}")); for entity_id in &request.entity_ids { let entity_feature_map = entity_features .entry(entity_id.clone()) .or_insert_with(HashMap::new); entity_feature_map.insert(feature_name.clone(), FeatureValue::Null); } } Err(e) => return Err(e), } } let latency = start_time.elapsed(); let latency_ms = latency.as_millis() as u64; // Check latency threshold if latency > self.config.max_latency { warnings.push(format!( "Batch request latency {}ms exceeded threshold {}ms", latency_ms, self.config.max_latency.as_millis() )); } // Update metrics if self.config.enable_metrics { self.update_metrics(latency, true, warnings.is_empty()) .await; } Ok(BatchFeatureResponse { entity_features, timestamp: Utc::now(), latency_ms, warnings, }) } /// Get current serving metrics pub async fn get_metrics(&self) -> ServingMetrics { if self.config.enable_metrics { let mut metrics = self.metrics.lock(); let latency_tracker = self.latency_tracker.lock(); metrics.avg_latency_ms = latency_tracker.average().as_millis() as f64; metrics.p95_latency_ms = latency_tracker.percentile(95.0).as_millis() as f64; metrics.p99_latency_ms = latency_tracker.percentile(99.0).as_millis() as f64; metrics.last_updated = Utc::now(); metrics.clone() } else { ServingMetrics::default() } } /// Reset serving metrics pub async fn reset_metrics(&self) { if self.config.enable_metrics { let mut metrics = self.metrics.lock(); *metrics = ServingMetrics::default(); let mut latency_tracker = self.latency_tracker.lock(); latency_tracker.samples.clear(); } } /// Update internal metrics async fn update_metrics(&self, latency: Duration, is_batch: bool, success: bool) { let mut metrics = self.metrics.lock(); let mut latency_tracker = self.latency_tracker.lock(); if is_batch { metrics.batch_requests += 1; } else { metrics.total_requests += 1; } latency_tracker.record(latency); if !success { metrics.error_count += 1; } } /// Health check endpoint pub async fn health_check(&self) -> Result { let store = self.store.write().await; // Try to list features to verify store connectivity let features_result = store.list_features().await; let store_healthy = features_result.is_ok(); let metrics = self.get_metrics().await; let latency_healthy = metrics.p99_latency_ms < (self.config.max_latency.as_millis() as f64 * 1.5); let status = if store_healthy && latency_healthy { HealthStatusType::Healthy } else if store_healthy { HealthStatusType::Degraded } else { HealthStatusType::Unhealthy }; Ok(HealthStatus { status, store_healthy, latency_healthy, metrics, timestamp: Utc::now(), }) } /// Access the underlying store #[must_use] pub fn store(&self) -> &Arc> { &self.store } /// Start the serving server (placeholder for actual HTTP server) pub async fn start(&self) -> Result<()> { tracing::info!( "Online feature server starting on port {}", self.config.port ); // In a real implementation, this would start an HTTP/gRPC server // For now, just return success Ok(()) } /// Stop the serving server pub async fn stop(&self) -> Result<()> { tracing::info!("Online feature server stopping"); Ok(()) } /// Prefetch features for upcoming requests (performance optimization) pub async fn prefetch_features( &self, feature_names: &[String], entity_ids: &[String], ) -> Result<()> { let mut store = self.store.write().await; // Prefetch features into cache for feature_name in feature_names { let _ = store.get_features_batch(feature_name, entity_ids).await; } Ok(()) } /// Warm up the cache with commonly accessed features pub async fn warmup_cache(&self, warmup_data: HashMap>) -> Result<()> { tracing::info!("Warming up cache with {} feature groups", warmup_data.len()); for (feature_name, entity_ids) in warmup_data { let _ = self.prefetch_features(&[feature_name], &entity_ids).await; } Ok(()) } /// Get feature serving statistics pub async fn get_feature_stats(&self, feature_name: &str) -> Result { let store = self.store.write().await; // Get feature metadata let metadata = store.get_feature_metadata(feature_name).await?; // In a real implementation, this would collect actual usage statistics Ok(FeatureStats { feature_name: feature_name.to_string(), request_count: 0, // Would be tracked in metrics cache_hit_rate: 0.0, // Would be calculated from cache metrics avg_latency_ms: 0.0, // Would be calculated from request metrics last_accessed: Utc::now(), schema: metadata.schema, }) } } /// Health status for the serving endpoint #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HealthStatus { /// Overall health status pub status: HealthStatusType, /// Whether the feature store is accessible pub store_healthy: bool, /// Whether latency is within acceptable bounds pub latency_healthy: bool, /// Current serving metrics pub metrics: ServingMetrics, /// Timestamp of the health check pub timestamp: DateTime, } /// Health status types #[derive(Debug, Clone, Serialize, Deserialize)] pub enum HealthStatusType { /// All systems operating normally Healthy, /// Some issues but still serving requests Degraded, /// Critical issues, unable to serve requests reliably Unhealthy, } /// Statistics for a specific feature #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureStats { /// Feature name pub feature_name: String, /// Number of requests for this feature pub request_count: u64, /// Cache hit rate for this feature pub cache_hit_rate: f64, /// Average latency for requests pub avg_latency_ms: f64, /// Last time this feature was accessed pub last_accessed: DateTime, /// Feature schema pub schema: serde_json::Value, } /// Real-time feature computation engine integration pub struct RealtimeCompute { compute_functions: HashMap< String, Box) -> Result + Send + Sync>, >, } impl RealtimeCompute { /// Create a new realtime compute engine #[must_use] pub fn new() -> Self { Self { compute_functions: HashMap::new(), } } /// Register a real-time computation function pub fn register_function(&mut self, feature_name: String, compute_fn: F) where F: Fn(&HashMap) -> Result + Send + Sync + 'static, { self.compute_functions .insert(feature_name, Box::new(compute_fn)); } /// Compute feature in real-time pub async fn compute_feature( &self, feature_name: &str, input_features: &HashMap, ) -> Result { if let Some(compute_fn) = self.compute_functions.get(feature_name) { compute_fn(input_features) } else { Err(FeatureStoreError::NotFound(format!( "No compute function registered for feature: {feature_name}" ))) } } } impl Default for RealtimeCompute { fn default() -> Self { Self::new() } } /// Feature serving cache with intelligent eviction pub struct ServingCache { cache: dashmap::DashMap, max_size: usize, ttl: Duration, } /// Cache entry with metadata #[derive(Debug, Clone)] struct CacheEntry { value: FeatureValue, timestamp: DateTime, access_count: u64, last_accessed: DateTime, } impl ServingCache { /// Create a new serving cache #[must_use] pub fn new(max_size: usize, ttl: Duration) -> Self { Self { cache: dashmap::DashMap::new(), max_size, ttl, } } /// Get a value from the cache #[must_use] pub fn get(&self, key: &str) -> Option { if let Some(mut entry) = self.cache.get_mut(key) { // Check if entry is still valid if Utc::now() - entry.timestamp < chrono::Duration::from_std(self.ttl).unwrap_or_default() { entry.access_count += 1; entry.last_accessed = Utc::now(); Some(entry.value.clone()) } else { // Entry expired, remove it drop(entry); self.cache.remove(key); None } } else { None } } /// Put a value into the cache pub fn put(&self, key: String, value: FeatureValue) { // Check if cache is full and evict LFU entry if self.cache.len() >= self.max_size { self.evict_lfu(); } let entry = CacheEntry { value, timestamp: Utc::now(), access_count: 1, last_accessed: Utc::now(), }; self.cache.insert(key, entry); } /// Evict least frequently used entry fn evict_lfu(&self) { let mut min_access_count = u64::MAX; let mut lfu_key = None; for item in &self.cache { if item.access_count < min_access_count { min_access_count = item.access_count; lfu_key = Some(item.key().clone()); } } if let Some(key) = lfu_key { self.cache.remove(&key); } } /// Get cache statistics #[must_use] pub fn stats(&self) -> CacheStats { let mut total_access_count = 0; let mut expired_count = 0; let current_time = Utc::now(); for entry in &self.cache { total_access_count += entry.access_count; if current_time - entry.timestamp >= chrono::Duration::from_std(self.ttl).unwrap_or_default() { expired_count += 1; } } CacheStats { size: self.cache.len(), max_size: self.max_size, total_access_count, expired_count, hit_rate: 0.0, // Would be calculated from request metrics } } /// Clear expired entries pub fn cleanup_expired(&self) { let current_time = Utc::now(); let ttl_chrono = chrono::Duration::from_std(self.ttl).unwrap_or_default(); self.cache .retain(|_, entry| current_time - entry.timestamp < ttl_chrono); } } /// Cache statistics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CacheStats { /// Current cache size pub size: usize, /// Maximum cache size pub max_size: usize, /// Total access count across all entries pub total_access_count: u64, /// Number of expired entries pub expired_count: usize, /// Cache hit rate (0.0 to 1.0) pub hit_rate: f64, } #[cfg(test)] mod tests { use super::*; use crate::store::{FeatureStoreConfig, InMemoryBackend}; #[tokio::test] #[ignore = "Pre-existing serving cache assertion failure"] async fn test_serving_cache() { let cache = ServingCache::new(3, Duration::from_secs(60)); // Test basic put/get cache.put("key1".to_string(), FeatureValue::Float(1.0)); assert_eq!(cache.get("key1"), Some(FeatureValue::Float(1.0))); // Test cache miss assert_eq!(cache.get("nonexistent"), None); // Test eviction cache.put("key2".to_string(), FeatureValue::Float(2.0)); cache.put("key3".to_string(), FeatureValue::Float(3.0)); cache.put("key4".to_string(), FeatureValue::Float(4.0)); // Should evict key1 assert_eq!(cache.get("key1"), None); assert_eq!(cache.get("key4"), Some(FeatureValue::Float(4.0))); // Test stats let stats = cache.stats(); assert_eq!(stats.size, 3); assert_eq!(stats.max_size, 3); } #[tokio::test] async fn test_latency_tracker() { let mut tracker = LatencyTracker::new(5); tracker.record(Duration::from_millis(10)); tracker.record(Duration::from_millis(20)); tracker.record(Duration::from_millis(30)); tracker.record(Duration::from_millis(40)); tracker.record(Duration::from_millis(50)); assert_eq!(tracker.average(), Duration::from_millis(30)); assert_eq!(tracker.percentile(95.0), Duration::from_millis(50)); assert_eq!(tracker.percentile(50.0), Duration::from_millis(30)); } #[tokio::test] async fn test_realtime_compute() { let mut compute = RealtimeCompute::new(); // Register a simple computation function compute.register_function("sum".to_string(), |inputs| { let a = match inputs.get("a") { Some(FeatureValue::Float(val)) => *val, _ => return Err(FeatureStoreError::Storage("Invalid input a".to_string())), }; let b = match inputs.get("b") { Some(FeatureValue::Float(val)) => *val, _ => return Err(FeatureStoreError::Storage("Invalid input b".to_string())), }; Ok(FeatureValue::Float(a + b)) }); // Test computation let mut inputs = HashMap::new(); inputs.insert("a".to_string(), FeatureValue::Float(10.0)); inputs.insert("b".to_string(), FeatureValue::Float(20.0)); let result = compute.compute_feature("sum", &inputs).await.unwrap(); assert_eq!(result, FeatureValue::Float(30.0)); } }