297 lines
No EOL
9.2 KiB
Rust
297 lines
No EOL
9.2 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::sync::RwLock;
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
/// Cache entry avec expiration
|
|
#[derive(Debug, Clone)]
|
|
pub struct CacheEntry<T> {
|
|
pub value: T,
|
|
pub expires_at: Instant,
|
|
pub hit_count: u64,
|
|
pub last_accessed: Instant,
|
|
}
|
|
|
|
impl<T> CacheEntry<T> {
|
|
pub fn new(value: T, ttl: Duration) -> Self {
|
|
let now = Instant::now();
|
|
Self {
|
|
value,
|
|
expires_at: now + ttl,
|
|
hit_count: 0,
|
|
last_accessed: now,
|
|
}
|
|
}
|
|
|
|
pub fn is_expired(&self) -> bool {
|
|
Instant::now() > self.expires_at
|
|
}
|
|
|
|
pub fn touch(&mut self) {
|
|
self.hit_count += 1;
|
|
self.last_accessed = Instant::now();
|
|
}
|
|
}
|
|
|
|
/// Cache intelligent avec LRU et expiration
|
|
pub struct SmartCache<K, V>
|
|
where
|
|
K: Clone + std::hash::Hash + Eq,
|
|
V: Clone,
|
|
{
|
|
entries: Arc<RwLock<HashMap<K, CacheEntry<V>>>>,
|
|
max_size: usize,
|
|
default_ttl: Duration,
|
|
}
|
|
|
|
impl<K, V> SmartCache<K, V>
|
|
where
|
|
K: Clone + std::hash::Hash + Eq,
|
|
V: Clone,
|
|
{
|
|
pub fn new(max_size: usize, default_ttl: Duration) -> Self {
|
|
Self {
|
|
entries: Arc::new(RwLock::new(HashMap::new())),
|
|
max_size,
|
|
default_ttl,
|
|
}
|
|
}
|
|
|
|
/// Insère une valeur dans le cache
|
|
pub async fn insert(&self, key: K, value: V) {
|
|
self.insert_with_ttl(key, value, self.default_ttl).await;
|
|
}
|
|
|
|
/// Insère une valeur avec un TTL personnalisé
|
|
pub async fn insert_with_ttl(&self, key: K, value: V, ttl: Duration) {
|
|
let mut entries = self.entries.write().await;
|
|
|
|
// Nettoyage des entrées expirées
|
|
self.cleanup_expired(&mut entries).await;
|
|
|
|
// Éviction LRU si le cache est plein
|
|
if entries.len() >= self.max_size {
|
|
self.evict_lru(&mut entries).await;
|
|
}
|
|
|
|
entries.insert(key, CacheEntry::new(value, ttl));
|
|
}
|
|
|
|
/// Récupère une valeur du cache
|
|
pub async fn get(&self, key: &K) -> Option<V> {
|
|
let mut entries = self.entries.write().await;
|
|
|
|
if let Some(entry) = entries.get_mut(key) {
|
|
if entry.is_expired() {
|
|
entries.remove(key);
|
|
return None;
|
|
}
|
|
|
|
entry.touch();
|
|
Some(entry.value.clone())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Supprime une entrée du cache
|
|
pub async fn remove(&self, key: &K) -> Option<V> {
|
|
let mut entries = self.entries.write().await;
|
|
entries.remove(key).map(|entry| entry.value)
|
|
}
|
|
|
|
/// Nettoie les entrées expirées
|
|
async fn cleanup_expired(&self, entries: &mut HashMap<K, CacheEntry<V>>) {
|
|
let expired_keys: Vec<K> = entries.iter()
|
|
.filter(|(_, entry)| entry.is_expired())
|
|
.map(|(key, _)| key.clone())
|
|
.collect();
|
|
|
|
for key in expired_keys {
|
|
entries.remove(&key);
|
|
}
|
|
}
|
|
|
|
/// Éviction LRU (Least Recently Used)
|
|
async fn evict_lru(&self, entries: &mut HashMap<K, CacheEntry<V>>) {
|
|
if let Some((lru_key, _)) = entries.iter()
|
|
.min_by_key(|(_, entry)| entry.last_accessed)
|
|
.map(|(key, entry)| (key.clone(), entry.clone())) {
|
|
entries.remove(&lru_key);
|
|
}
|
|
}
|
|
|
|
/// Statistiques du cache
|
|
pub async fn stats(&self) -> CacheStats {
|
|
let entries = self.entries.read().await;
|
|
let total_hits: u64 = entries.values().map(|entry| entry.hit_count).sum();
|
|
|
|
CacheStats {
|
|
total_entries: entries.len(),
|
|
max_size: self.max_size,
|
|
total_hits,
|
|
hit_rate: if entries.is_empty() { 0.0 } else { total_hits as f64 / entries.len() as f64 },
|
|
}
|
|
}
|
|
|
|
/// Vide le cache
|
|
pub async fn clear(&self) {
|
|
let mut entries = self.entries.write().await;
|
|
entries.clear();
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct CacheStats {
|
|
pub total_entries: usize,
|
|
pub max_size: usize,
|
|
pub total_hits: u64,
|
|
pub hit_rate: f64,
|
|
}
|
|
|
|
/// Cache spécialisé pour les messages de salon
|
|
pub type RoomMessageCache = SmartCache<String, Vec<MessageCacheEntry>>;
|
|
|
|
/// Cache spécialisé pour les messages directs
|
|
pub type DirectMessageCache = SmartCache<(i32, i32), Vec<MessageCacheEntry>>;
|
|
|
|
/// Cache spécialisé pour les utilisateurs en ligne
|
|
pub type UserPresenceCache = SmartCache<i32, UserPresenceEntry>;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MessageCacheEntry {
|
|
pub id: i32,
|
|
pub user_id: i32,
|
|
pub username: String,
|
|
pub content: String,
|
|
pub timestamp: chrono::DateTime<chrono::Utc>,
|
|
pub message_type: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UserPresenceEntry {
|
|
pub user_id: i32,
|
|
pub username: String,
|
|
pub status: String,
|
|
pub last_seen: chrono::DateTime<chrono::Utc>,
|
|
pub current_room: Option<String>,
|
|
}
|
|
|
|
/// Gestionnaire centralisé de tous les caches
|
|
pub struct CacheManager {
|
|
pub room_messages: RoomMessageCache,
|
|
pub direct_messages: DirectMessageCache,
|
|
pub user_presence: UserPresenceCache,
|
|
pub user_sessions: SmartCache<String, i32>, // JWT token -> user_id
|
|
}
|
|
|
|
impl Default for CacheManager {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl CacheManager {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
// Cache des messages de salon (30 min TTL)
|
|
room_messages: SmartCache::new(1000, Duration::from_secs(1800)),
|
|
|
|
// Cache des messages directs (1 heure TTL)
|
|
direct_messages: SmartCache::new(500, Duration::from_secs(3600)),
|
|
|
|
// Cache de présence utilisateur (5 min TTL)
|
|
user_presence: SmartCache::new(10000, Duration::from_secs(300)),
|
|
|
|
// Cache des sessions JWT (24 heures TTL)
|
|
user_sessions: SmartCache::new(50000, Duration::from_secs(86400)),
|
|
}
|
|
}
|
|
|
|
/// Met en cache les messages d'un salon
|
|
pub async fn cache_room_messages(&self, room: &str, messages: Vec<MessageCacheEntry>) {
|
|
self.room_messages.insert(room.to_string(), messages).await;
|
|
}
|
|
|
|
/// Récupère les messages mis en cache d'un salon
|
|
pub async fn get_cached_room_messages(&self, room: &str) -> Option<Vec<MessageCacheEntry>> {
|
|
self.room_messages.get(&room.to_string()).await
|
|
}
|
|
|
|
/// Met en cache les messages directs entre deux utilisateurs
|
|
pub async fn cache_direct_messages(&self, user1: i32, user2: i32, messages: Vec<MessageCacheEntry>) {
|
|
// Normaliser la clé pour éviter les doublons (user1, user2) et (user2, user1)
|
|
let key = if user1 < user2 { (user1, user2) } else { (user2, user1) };
|
|
self.direct_messages.insert(key, messages).await;
|
|
}
|
|
|
|
/// Récupère les messages directs mis en cache
|
|
pub async fn get_cached_direct_messages(&self, user1: i32, user2: i32) -> Option<Vec<MessageCacheEntry>> {
|
|
let key = if user1 < user2 { (user1, user2) } else { (user2, user1) };
|
|
self.direct_messages.get(&key).await
|
|
}
|
|
|
|
/// Met en cache la présence d'un utilisateur
|
|
pub async fn cache_user_presence(&self, user_id: i32, presence: UserPresenceEntry) {
|
|
self.user_presence.insert(user_id, presence).await;
|
|
}
|
|
|
|
/// Récupère la présence mise en cache d'un utilisateur
|
|
pub async fn get_cached_user_presence(&self, user_id: i32) -> Option<UserPresenceEntry> {
|
|
self.user_presence.get(&user_id).await
|
|
}
|
|
|
|
/// Met en cache une session utilisateur
|
|
pub async fn cache_user_session(&self, token: &str, user_id: i32) {
|
|
self.user_sessions.insert(token.to_string(), user_id).await;
|
|
}
|
|
|
|
/// Récupère l'ID utilisateur d'un token mis en cache
|
|
pub async fn get_cached_user_session(&self, token: &str) -> Option<i32> {
|
|
self.user_sessions.get(&token.to_string()).await
|
|
}
|
|
|
|
/// Invalide la session d'un utilisateur
|
|
pub async fn invalidate_user_session(&self, token: &str) {
|
|
self.user_sessions.remove(&token.to_string()).await;
|
|
}
|
|
|
|
/// Nettoie tous les caches expirés
|
|
pub async fn cleanup_all(&self) {
|
|
// Le nettoyage est automatique lors des opérations get/insert
|
|
tracing::info!("🧹 Nettoyage automatique des caches effectué");
|
|
}
|
|
|
|
/// Statistiques globales des caches
|
|
pub async fn global_stats(&self) -> GlobalCacheStats {
|
|
let room_stats = self.room_messages.stats().await;
|
|
let dm_stats = self.direct_messages.stats().await;
|
|
let presence_stats = self.user_presence.stats().await;
|
|
let session_stats = self.user_sessions.stats().await;
|
|
|
|
GlobalCacheStats {
|
|
room_messages: room_stats,
|
|
direct_messages: dm_stats,
|
|
user_presence: presence_stats,
|
|
user_sessions: session_stats,
|
|
}
|
|
}
|
|
|
|
/// Vide tous les caches (pour le débogage/maintenance)
|
|
pub async fn clear_all(&self) {
|
|
self.room_messages.clear().await;
|
|
self.direct_messages.clear().await;
|
|
self.user_presence.clear().await;
|
|
self.user_sessions.clear().await;
|
|
tracing::warn!("🗑️ Tous les caches ont été vidés");
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct GlobalCacheStats {
|
|
pub room_messages: CacheStats,
|
|
pub direct_messages: CacheStats,
|
|
pub user_presence: CacheStats,
|
|
pub user_sessions: CacheStats,
|
|
}
|