veza/veza-chat-server/src/cache.rs
2025-12-03 20:33:26 +01:00

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,
}