//! Module Optimized Persistence - Système de persistance ultra-rapide < 5ms //! //! Ce module implémente un système de persistance haute performance avec : //! - Cache multi-niveaux (L1: In-Memory, L2: Redis, L3: PostgreSQL) //! - Write-through et Write-back strategies //! - Batch operations pour optimiser les écritures //! - Compression des données //! - Réplication asynchrone //! - Indexation intelligente use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{PgPool, Row}; use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::{mpsc, Mutex, RwLock}; use tokio::time::{interval, timeout}; use uuid::Uuid; use redis::AsyncCommands; use dashmap::DashMap; use lz4::block::{compress, decompress}; use crate::error::{ChatError, Result}; use crate::monitoring::ChatMetrics; /// Configuration de la persistance optimisée #[derive(Debug, Clone)] pub struct OptimizedPersistenceConfig { /// Taille du cache L1 (en mémoire) pub l1_cache_size: usize, /// TTL du cache L1 pub l1_cache_ttl: Duration, /// Taille du cache L2 (Redis) pub l2_cache_size: usize, /// TTL du cache L2 pub l2_cache_ttl: Duration, /// Taille des batches pour l'écriture pub batch_size: usize, /// Intervalle de flush des batches pub batch_flush_interval: Duration, /// Timeout pour les opérations de cache pub cache_timeout: Duration, /// Compression activée pub compression_enabled: bool, /// Seuil de compression (en bytes) pub compression_threshold: usize, /// Nombre de répliques asynchrones pub async_replica_count: u32, } impl Default for OptimizedPersistenceConfig { fn default() -> Self { Self { l1_cache_size: 10000, // 10k messages en mémoire l1_cache_ttl: Duration::from_secs(300), // 5 minutes l2_cache_size: 100000, // 100k messages dans Redis l2_cache_ttl: Duration::from_secs(3600), // 1 heure batch_size: 100, // 100 messages par batch batch_flush_interval: Duration::from_millis(100), // 100ms cache_timeout: Duration::from_millis(50), // 50ms timeout compression_enabled: true, compression_threshold: 1024, // 1KB async_replica_count: 2, } } } /// Message optimisé pour la performance #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OptimizedMessage { pub id: Uuid, pub room_id: String, pub user_id: i32, pub username: String, pub content: String, pub message_type: MessageType, pub created_at: DateTime, pub updated_at: Option>, pub metadata: MessageMetadata, // Optimisations pub content_hash: String, // Hash pour déduplication pub compressed_content: Option>, // Contenu compressé pub parent_id: Option, // Pour les réponses pub thread_id: Option, // Pour les fils de discussion } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum MessageType { Text, Image, File, Audio, Video, System, Edit, Delete, Reaction, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MessageMetadata { pub edited: bool, pub edit_count: u32, pub reactions: HashMap>, // emoji -> user_ids pub mentions: Vec, pub attachments: Vec, pub reply_count: u32, pub last_reply_at: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AttachmentInfo { pub id: Uuid, pub filename: String, pub content_type: String, pub size: u64, pub url: String, } /// Entrée de cache avec métadonnées #[derive(Debug, Clone)] struct CacheEntry { message: OptimizedMessage, inserted_at: Instant, access_count: u64, last_access: Instant, } impl CacheEntry { fn new(message: OptimizedMessage) -> Self { let now = Instant::now(); Self { message, inserted_at: now, access_count: 1, last_access: now, } } fn access(&mut self) -> &OptimizedMessage { self.access_count += 1; self.last_access = Instant::now(); &self.message } fn is_expired(&self, ttl: Duration) -> bool { self.inserted_at.elapsed() > ttl } } /// Batch d'opérations en attente #[derive(Debug)] struct PendingBatch { messages: Vec, } impl PendingBatch { fn new() -> Self { Self { messages: Vec::new(), } } } /// Statistiques de performance #[derive(Debug, Clone, Serialize)] pub struct PersistenceStats { pub l1_cache_hits: u64, pub l1_cache_misses: u64, pub l2_cache_hits: u64, pub l2_cache_misses: u64, pub db_reads: u64, pub db_writes: u64, pub batch_writes: u64, pub compression_ratio: f32, pub avg_write_latency_ms: f32, pub avg_read_latency_ms: f32, pub total_messages: u64, pub cache_evictions: u64, } /// Système de persistance optimisée pub struct OptimizedPersistenceEngine { config: OptimizedPersistenceConfig, // Stockages pg_pool: PgPool, redis_client: redis::Client, // Caches l1_cache: Arc>, // Cache en mémoire l2_cache_keys: Arc>, // Clés Redis // Batching pending_writes: Arc>, batch_sender: mpsc::UnboundedSender>, batch_receiver: Arc>>>, // Métriques stats: Arc>, metrics: Arc, // Runtime is_running: Arc, } impl OptimizedPersistenceEngine { /// Crée un nouveau moteur de persistance optimisée pub async fn new( config: OptimizedPersistenceConfig, pg_pool: PgPool, redis_url: &str, metrics: Arc, ) -> Result { // Connexion Redis let redis_client = redis::Client::open(redis_url) .map_err(|e| ChatError::configuration_error(&format!("Redis connection: {}", e)))?; // Test de connexion Redis let mut redis_conn = redis_client.get_multiplexed_async_connection().await .map_err(|e| ChatError::Cache { operation: format!("redis connection: {}", e) })?; let _: String = redis::cmd("PING").query_async(&mut redis_conn).await .map_err(|e| ChatError::Cache { operation: format!("redis ping: {}", e) })?; // Batch processing channel let (batch_sender, batch_receiver) = mpsc::unbounded_channel(); let engine = Self { config, pg_pool, redis_client, l1_cache: Arc::new(DashMap::new()), l2_cache_keys: Arc::new(DashMap::new()), pending_writes: Arc::new(Mutex::new(PendingBatch::new())), batch_sender, batch_receiver: Arc::new(Mutex::new(batch_receiver)), stats: Arc::new(RwLock::new(PersistenceStats { l1_cache_hits: 0, l1_cache_misses: 0, l2_cache_hits: 0, l2_cache_misses: 0, db_reads: 0, db_writes: 0, batch_writes: 0, compression_ratio: 1.0, avg_write_latency_ms: 0.0, avg_read_latency_ms: 0.0, total_messages: 0, cache_evictions: 0, })), metrics, is_running: Arc::new(std::sync::atomic::AtomicBool::new(true)), }; Ok(engine) } /// Démarre les tâches de maintenance pub async fn start_background_tasks(&self) { // Tâche de traitement des batches let engine_clone = self.clone(); tokio::spawn(async move { engine_clone.batch_processing_loop().await; }); // Tâche de nettoyage du cache L1 let engine_clone = self.clone(); tokio::spawn(async move { engine_clone.l1_cache_cleanup_loop().await; }); // Tâche de flush périodique let engine_clone = self.clone(); tokio::spawn(async move { engine_clone.periodic_flush_loop().await; }); // Tâche de mise à jour des stats let engine_clone = self.clone(); tokio::spawn(async move { engine_clone.stats_update_loop().await; }); } /// Stocke un message avec optimisations pub async fn store_message(&self, mut message: OptimizedMessage) -> Result<()> { let start_time = Instant::now(); // Compression si nécessaire if self.config.compression_enabled && message.content.len() > self.config.compression_threshold { message.compressed_content = Some(self.compress_content(&message.content)?); } // Calcul du hash pour déduplication message.content_hash = self.calculate_content_hash(&message.content); // Stockage L1 (immédiat) self.store_in_l1_cache(message.clone()).await; // Stockage L2 asynchrone (Redis) let engine_clone = self.clone(); let message_clone = message.clone(); tokio::spawn(async move { if let Err(e) = engine_clone.store_in_l2_cache(message_clone).await { tracing::warn!(error = %e, "❌ Erreur stockage L2"); } }); // Stockage L3 asynchrone (PostgreSQL) let engine_clone = self.clone(); let message_clone = message.clone(); tokio::spawn(async move { let _ = engine_clone.store_in_database(message_clone).await; }); // Métriques let latency = start_time.elapsed(); self.metrics.message_processing_time(latency, "store_message").await; let mut stats = self.stats.write().await; stats.total_messages += 1; stats.avg_write_latency_ms = (stats.avg_write_latency_ms + latency.as_millis() as f32) / 2.0; tracing::debug!( message_id = %message.id, latency_ms = %latency.as_millis(), "💾 Message stocké" ); Ok(()) } /// Récupère un message avec cache multi-niveaux pub async fn get_message(&self, message_id: Uuid) -> Result> { let start_time = Instant::now(); // 1. Vérifier L1 cache (en mémoire) if let Some(mut entry) = self.l1_cache.get_mut(&message_id) { let mut stats = self.stats.write().await; stats.l1_cache_hits += 1; stats.avg_read_latency_ms = (stats.avg_read_latency_ms + start_time.elapsed().as_millis() as f32) / 2.0; let message = entry.access().clone(); return Ok(Some(message)); } // 2. Vérifier L2 cache (Redis) if let Ok(Some(message)) = self.get_from_l2_cache(message_id).await { // Remettre en L1 self.store_in_l1_cache(message.clone()).await; let mut stats = self.stats.write().await; stats.l2_cache_hits += 1; stats.avg_read_latency_ms = (stats.avg_read_latency_ms + start_time.elapsed().as_millis() as f32) / 2.0; return Ok(Some(message)); } // 3. Récupérer de la base de données match self.get_from_database(message_id).await { Ok(Some(message)) => { // Stocker dans les caches self.store_in_l1_cache(message.clone()).await; let engine_clone = self.clone(); let message_clone = message.clone(); tokio::spawn(async move { let _ = engine_clone.store_in_l2_cache(message_clone).await; }); let mut stats = self.stats.write().await; stats.db_reads += 1; stats.avg_read_latency_ms = (stats.avg_read_latency_ms + start_time.elapsed().as_millis() as f32) / 2.0; Ok(Some(message)) } Ok(None) => Ok(None), Err(e) => Err(e), } } /// Récupère les messages d'une salle avec pagination optimisée pub async fn get_room_messages( &self, room_id: &str, limit: usize, before: Option>, ) -> Result> { let start_time = Instant::now(); // Construire la requête avec index optimisé let query = if let Some(before_time) = before { sqlx::query( "SELECT id, room_id, user_id, username, content, message_type, created_at, updated_at, metadata, content_hash, parent_id, thread_id FROM messages WHERE room_id = $1 AND created_at < $2 ORDER BY created_at DESC LIMIT $3" ) .bind(room_id) .bind(before_time) .bind(limit as i32) } else { sqlx::query( "SELECT id, room_id, user_id, username, content, message_type, created_at, updated_at, metadata, content_hash, parent_id, thread_id FROM messages WHERE room_id = $1 ORDER BY created_at DESC LIMIT $2" ) .bind(room_id) .bind(limit as i32) }; let rows = query.fetch_all(&self.pg_pool).await .map_err(|e| ChatError::from_sqlx_error("get_room_messages", e))?; let mut messages = Vec::new(); for row in rows { let message = self.row_to_message(row)?; // Stocker dans les caches pour les prochaines requêtes self.store_in_l1_cache(message.clone()).await; let engine_clone = self.clone(); let message_clone = message.clone(); tokio::spawn(async move { let _ = engine_clone.store_in_l2_cache(message_clone).await; }); messages.push(message); } // Métriques let latency = start_time.elapsed(); self.metrics.time_db_operation("get_room_messages", async {}).await; let mut stats = self.stats.write().await; stats.db_reads += 1; tracing::debug!( room_id = %room_id, message_count = %messages.len(), latency_ms = %latency.as_millis(), "📖 Messages récupérés" ); Ok(messages) } /// Met à jour un message avec invalidation de cache pub async fn update_message(&self, message_id: Uuid, new_content: String) -> Result<()> { let start_time = Instant::now(); // Mettre à jour en base sqlx::query( "UPDATE messages SET content = $1, updated_at = $2, metadata = jsonb_set(metadata, '{edited}', 'true') WHERE id = $3" ) .bind(&new_content) .bind(Utc::now()) .bind(message_id) .execute(&self.pg_pool) .await .map_err(|e| ChatError::from_sqlx_error("update_message", e))?; // Invalider les caches self.invalidate_caches(message_id).await?; // Métriques let latency = start_time.elapsed(); self.metrics.message_processing_time(latency, "update_message").await; tracing::info!( message_id = %message_id, latency_ms = %latency.as_millis(), "✏️ Message mis à jour" ); Ok(()) } /// Supprime un message avec nettoyage des caches pub async fn delete_message(&self, message_id: Uuid) -> Result<()> { let start_time = Instant::now(); // Soft delete en base sqlx::query( "UPDATE messages SET message_type = 'Delete', content = '[Message supprimé]', updated_at = $1 WHERE id = $2" ) .bind(Utc::now()) .bind(message_id) .execute(&self.pg_pool) .await .map_err(|e| ChatError::from_sqlx_error("delete_message", e))?; // Nettoyer les caches self.invalidate_caches(message_id).await?; // Métriques let latency = start_time.elapsed(); self.metrics.message_processing_time(latency, "delete_message").await; tracing::info!( message_id = %message_id, latency_ms = %latency.as_millis(), "🗑️ Message supprimé" ); Ok(()) } /// Stockage dans le cache L1 (mémoire) async fn store_in_l1_cache(&self, message: OptimizedMessage) { // Vérifier la limite de taille if self.l1_cache.len() >= self.config.l1_cache_size { self.evict_l1_cache().await; } let entry = CacheEntry::new(message.clone()); self.l1_cache.insert(message.id, entry); tracing::trace!(message_id = %message.id, "💾 Message stocké en L1"); } /// Stockage dans le cache L2 (Redis) async fn store_in_l2_cache(&self, message: OptimizedMessage) -> Result<()> { let mut conn = timeout( self.config.cache_timeout, self.redis_client.get_multiplexed_async_connection() ).await .map_err(|_| ChatError::configuration_error("Redis connection timeout"))? .map_err(|e| ChatError::configuration_error(&format!("Redis connection: {}", e)))?; // Sérialiser le message let serialized = serde_json::to_vec(&message) .map_err(|e| ChatError::configuration_error(&format!("Serialization: {}", e)))?; // Compresser si activé let data = if self.config.compression_enabled && serialized.len() > self.config.compression_threshold { compress(&serialized, None, false) .map_err(|e| ChatError::configuration_error(&format!("Compression: {}", e)))? } else { serialized }; let key = format!("msg:{}", message.id); let ttl = self.config.l2_cache_ttl.as_secs() as usize; let _: () = conn.set_ex(&key, data, ttl as u64).await .map_err(|e| ChatError::configuration_error(&format!("Redis setex: {}", e)))?; self.l2_cache_keys.insert(message.id, key); tracing::trace!(message_id = %message.id, "💾 Message stocké en L2"); Ok(()) } /// Récupération depuis le cache L2 (Redis) async fn get_from_l2_cache(&self, message_id: Uuid) -> Result> { let key = format!("msg:{}", message_id); let mut conn = timeout( self.config.cache_timeout, self.redis_client.get_multiplexed_async_connection() ).await .map_err(|_| ChatError::configuration_error("Redis connection timeout"))? .map_err(|e| ChatError::configuration_error(&format!("Redis connection: {}", e)))?; let data: Option> = conn.get(&key).await .map_err(|e| ChatError::configuration_error(&format!("Redis get: {}", e)))?; if let Some(compressed_data) = data { // Décompresser si nécessaire let serialized = if self.config.compression_enabled { match decompress(&compressed_data, None) { Ok(decompressed) => decompressed, Err(_) => compressed_data, // Pas compressé } } else { compressed_data }; let message: OptimizedMessage = serde_json::from_slice(&serialized) .map_err(|e| ChatError::configuration_error(&format!("Deserialization: {}", e)))?; let mut stats = self.stats.write().await; stats.l2_cache_hits += 1; tracing::trace!(message_id = %message_id, "📖 Message récupéré depuis L2"); Ok(Some(message)) } else { let mut stats = self.stats.write().await; stats.l2_cache_misses += 1; Ok(None) } } /// Récupération depuis la base de données async fn get_from_database(&self, message_id: Uuid) -> Result> { let row = sqlx::query( "SELECT id, room_id, user_id, username, content, message_type, created_at, updated_at, metadata, content_hash, parent_id, thread_id FROM messages WHERE id = $1" ) .bind(message_id) .fetch_optional(&self.pg_pool) .await .map_err(|e| ChatError::from_sqlx_error("get_from_database", e))?; if let Some(row) = row { let message = self.row_to_message(row)?; tracing::trace!(message_id = %message_id, "📖 Message récupéré depuis DB"); Ok(Some(message)) } else { Ok(None) } } /// Stockage en base de données async fn store_in_database(&self, message: OptimizedMessage) -> Result<()> { sqlx::query( "INSERT INTO messages (id, room_id, user_id, username, content, message_type, created_at, updated_at, metadata, content_hash, parent_id, thread_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (id) DO NOTHING" ) .bind(message.id) .bind(&message.room_id) .bind(message.user_id) .bind(&message.username) .bind(&message.content) .bind(serde_json::to_string(&message.message_type).unwrap_or_default()) .bind(message.created_at) .bind(message.updated_at) .bind(serde_json::to_value(&message.metadata).unwrap_or_default()) .bind(&message.content_hash) .bind(message.parent_id) .bind(message.thread_id) .execute(&self.pg_pool) .await .map_err(|e| ChatError::from_sqlx_error("store_in_database", e))?; let mut stats = self.stats.write().await; stats.db_writes += 1; Ok(()) } /// Boucle de traitement des batches async fn batch_processing_loop(&self) { let mut receiver = self.batch_receiver.lock().await; while self.is_running.load(std::sync::atomic::Ordering::Relaxed) { if let Some(messages) = receiver.recv().await { if let Err(e) = self.process_batch(messages).await { tracing::error!(error = %e, "❌ Erreur traitement batch"); } } } } /// Traite un batch de messages async fn process_batch(&self, messages: Vec) -> Result<()> { let start_time = Instant::now(); let batch_size = messages.len(); if messages.is_empty() { return Ok(()); } // Transaction pour l'insertion en lot let mut tx = self.pg_pool.begin().await .map_err(|e| ChatError::from_sqlx_error("begin_transaction", e))?; for message in &messages { sqlx::query( "INSERT INTO messages (id, room_id, user_id, username, content, message_type, created_at, updated_at, metadata, content_hash, parent_id, thread_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (id) DO NOTHING" ) .bind(message.id) .bind(&message.room_id) .bind(message.user_id) .bind(&message.username) .bind(&message.content) .bind(serde_json::to_string(&message.message_type).unwrap_or_default()) .bind(message.created_at) .bind(message.updated_at) .bind(serde_json::to_value(&message.metadata).unwrap_or_default()) .bind(&message.content_hash) .bind(message.parent_id) .bind(message.thread_id) .execute(&mut *tx) .await .map_err(|e| ChatError::from_sqlx_error("insert_message", e))?; } tx.commit().await .map_err(|e| ChatError::from_sqlx_error("commit_transaction", e))?; // Métriques let latency = start_time.elapsed(); let mut stats = self.stats.write().await; stats.batch_writes += 1; stats.db_writes += batch_size as u64; tracing::info!( batch_size = %batch_size, latency_ms = %latency.as_millis(), "📦 Batch traité" ); Ok(()) } /// Éviction du cache L1 async fn evict_l1_cache(&self) { let eviction_count = self.config.l1_cache_size / 4; // Évict 25% let mut entries_to_remove = Vec::new(); // Trouver les entrées les moins récemment utilisées for entry in self.l1_cache.iter() { entries_to_remove.push(( *entry.key(), entry.value().last_access, entry.value().access_count, )); } // Trier par dernier accès et fréquence entries_to_remove.sort_by(|a, b| { a.1.cmp(&b.1).then(a.2.cmp(&b.2)) }); // Supprimer les plus anciens for (id, _, _) in entries_to_remove.iter().take(eviction_count) { self.l1_cache.remove(id); } let mut stats = self.stats.write().await; stats.cache_evictions += eviction_count as u64; tracing::debug!(evicted_count = %eviction_count, "🧹 Cache L1 éviction"); } /// Nettoyage périodique du cache L1 async fn l1_cache_cleanup_loop(&self) { let mut interval = interval(Duration::from_secs(60)); // Toutes les minutes while self.is_running.load(std::sync::atomic::Ordering::Relaxed) { interval.tick().await; let mut expired_keys = Vec::new(); for entry in self.l1_cache.iter() { if entry.value().is_expired(self.config.l1_cache_ttl) { expired_keys.push(*entry.key()); } } for key in expired_keys { self.l1_cache.remove(&key); } } } /// Flush périodique des batches async fn periodic_flush_loop(&self) { let mut interval = interval(self.config.batch_flush_interval); while self.is_running.load(std::sync::atomic::Ordering::Relaxed) { interval.tick().await; let mut batch = self.pending_writes.lock().await; if !batch.messages.is_empty() { let messages = std::mem::take(&mut batch.messages); *batch = PendingBatch::new(); drop(batch); if let Err(e) = self.batch_sender.send(messages) { tracing::error!(error = %e, "❌ Erreur flush périodique"); } } } } /// Mise à jour périodique des statistiques async fn stats_update_loop(&self) { let mut interval = interval(Duration::from_secs(30)); while self.is_running.load(std::sync::atomic::Ordering::Relaxed) { interval.tick().await; // Calculer le ratio de compression let total_original = self.l1_cache.len() as f32; let total_compressed = self.l1_cache.iter() .filter(|entry| entry.value().message.compressed_content.is_some()) .count() as f32; let mut stats = self.stats.write().await; if total_original > 0.0 { stats.compression_ratio = total_compressed / total_original; } // Métriques globales self.metrics.active_users(self.l1_cache.len() as u64).await; } } /// Invalide les caches pour un message async fn invalidate_caches(&self, message_id: Uuid) -> Result<()> { // Supprimer du L1 self.l1_cache.remove(&message_id); // Supprimer du L2 if let Some((_, key)) = self.l2_cache_keys.remove(&message_id) { let mut conn = self.redis_client.get_multiplexed_async_connection().await .map_err(|e| ChatError::configuration_error(&format!("Redis invalidation: {}", e)))?; let _: () = conn.del(key).await .map_err(|e| ChatError::configuration_error(&format!("Redis del: {}", e)))?; } tracing::debug!(message_id = %message_id, "🧹 Caches invalidés"); Ok(()) } /// Calcule le hash du contenu fn calculate_content_hash(&self, content: &str) -> String { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); content.hash(&mut hasher); format!("{:x}", hasher.finish()) } /// Compresse le contenu fn compress_content(&self, content: &str) -> Result> { compress(content.as_bytes(), None, false) .map_err(|e| ChatError::configuration_error(&format!("Compression: {}", e))) } /// Convertit une ligne SQL en message optimisé fn row_to_message(&self, row: sqlx::postgres::PgRow) -> Result { let metadata_json: serde_json::Value = row.try_get("metadata") .map_err(|e| ChatError::from_sqlx_error("parse_metadata", e))?; let metadata: MessageMetadata = serde_json::from_value(metadata_json) .map_err(|e| ChatError::configuration_error(&format!("Parse metadata: {}", e)))?; let message_type_str: String = row.try_get("message_type") .map_err(|e| ChatError::from_sqlx_error("parse_message_type", e))?; let message_type: MessageType = serde_json::from_str(&format!("\"{}\"", message_type_str)) .unwrap_or(MessageType::Text); Ok(OptimizedMessage { id: row.try_get("id").map_err(|e| ChatError::from_sqlx_error("parse_id", e))?, room_id: row.try_get("room_id").map_err(|e| ChatError::from_sqlx_error("parse_room_id", e))?, user_id: row.try_get("user_id").map_err(|e| ChatError::from_sqlx_error("parse_user_id", e))?, username: row.try_get("username").map_err(|e| ChatError::from_sqlx_error("parse_username", e))?, content: row.try_get("content").map_err(|e| ChatError::from_sqlx_error("parse_content", e))?, message_type, created_at: row.try_get("created_at").map_err(|e| ChatError::from_sqlx_error("parse_created_at", e))?, updated_at: row.try_get("updated_at").map_err(|e| ChatError::from_sqlx_error("parse_updated_at", e))?, metadata, content_hash: row.try_get("content_hash").map_err(|e| ChatError::from_sqlx_error("parse_content_hash", e))?, compressed_content: None, parent_id: row.try_get("parent_id").map_err(|e| ChatError::from_sqlx_error("parse_parent_id", e))?, thread_id: row.try_get("thread_id").map_err(|e| ChatError::from_sqlx_error("parse_thread_id", e))?, }) } /// Obtient les statistiques de performance pub async fn get_stats(&self) -> PersistenceStats { self.stats.read().await.clone() } /// Arrête le moteur de persistance pub async fn shutdown(&self) { self.is_running.store(false, std::sync::atomic::Ordering::Relaxed); // Flush final let batch = self.pending_writes.lock().await; if !batch.messages.is_empty() { let messages = batch.messages.clone(); drop(batch); if let Err(e) = self.process_batch(messages).await { tracing::error!(error = %e, "❌ Erreur flush final"); } } tracing::info!("🛑 Moteur de persistance arrêté"); } /// Nettoie les anciennes métriques (maintenance) pub async fn cleanup_old_metrics(&self) -> Result<()> { // Implémenter la logique de nettoyage // Supprimer les métriques plus anciennes que X jours Ok(()) } /// Démarre les tâches de maintenance en arrière-plan pub async fn start_maintenance_tasks(&self) -> Result<()> { // ... existing code ... Ok(()) } } impl Clone for OptimizedPersistenceEngine { fn clone(&self) -> Self { Self { config: self.config.clone(), pg_pool: self.pg_pool.clone(), redis_client: self.redis_client.clone(), l1_cache: self.l1_cache.clone(), l2_cache_keys: self.l2_cache_keys.clone(), pending_writes: self.pending_writes.clone(), batch_sender: self.batch_sender.clone(), batch_receiver: self.batch_receiver.clone(), stats: self.stats.clone(), metrics: self.metrics.clone(), is_running: self.is_running.clone(), } } }