//! In-memory cache implementation with TTL support. //! //! Provides a thread-safe, in-memory cache with automatic expiration. //! Used as a fallback when Redis is unavailable or for local development. use super::Cache; use anyhow::Result; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; use tracing::{debug, warn}; /// Cache entry: (serialized_value, optional_expiry) type CacheEntry = (String, Option); /// In-memory cache implementation with TTL support. /// /// Uses Arc> for thread-safe concurrent access. /// Expired entries are checked on read but not automatically cleaned up. pub struct InMemoryCache { data: Arc>>, default_ttl_seconds: u64, } impl InMemoryCache { /// Create a new in-memory cache with optional default TTL. /// /// # Arguments /// * `default_ttl_seconds` - Default expiration time in seconds (default: 3600) pub fn new(default_ttl_seconds: Option) -> Self { Self { data: Arc::new(RwLock::new(HashMap::new())), default_ttl_seconds: default_ttl_seconds.unwrap_or(3600), } } } #[async_trait] impl Cache for InMemoryCache { async fn get(&mut self, key: &str) -> Result> where T: for<'de> Deserialize<'de> + Send, { let data = self.data.read().await; if let Some((serialized, expiry)) = data.get(key) { if let Some(exp) = expiry && *exp <= Instant::now() { debug!(cache_key = %key, "Cache entry expired"); return Ok(None); } match serde_json::from_str::(serialized) { Ok(value) => Ok(Some(value)), Err(e) => { warn!( error = ?e, cache_key = %key, "Failed to deserialize cached value" ); Ok(None) } } } else { Ok(None) } } async fn set(&mut self, key: &str, value: &T, ttl_seconds: Option) -> Result<()> where T: Serialize + Send + Sync, { let ttl = ttl_seconds.unwrap_or(self.default_ttl_seconds); match serde_json::to_string(value) { Ok(serialized) => { let expiry = if ttl > 0 { Some(Instant::now() + Duration::from_secs(ttl)) } else { None }; let mut data = self.data.write().await; data.insert(key.to_string(), (serialized, expiry)); debug!( cache_key = %key, ttl_seconds = ttl, "Cached value in memory" ); Ok(()) } Err(e) => { warn!( error = ?e, cache_key = %key, "Failed to serialize value for caching" ); Ok(()) } } } async fn delete(&mut self, key: &str) -> Result<()> { let mut data = self.data.write().await; data.remove(key); debug!(cache_key = %key, "Deleted key from in-memory cache"); Ok(()) } async fn set_multiple(&mut self, items: Vec<(&str, &T, Option)>) -> Result<()> where T: Serialize + Send + Sync, { if items.is_empty() { return Ok(()); } let mut data = self.data.write().await; let mut success_count = 0; for (key, value, ttl) in &items { match serde_json::to_string(value) { Ok(serialized) => { let ttl_to_use = ttl.unwrap_or(self.default_ttl_seconds); let expiry = if ttl_to_use > 0 { Some(Instant::now() + Duration::from_secs(ttl_to_use)) } else { None }; data.insert(key.to_string(), (serialized, expiry)); success_count += 1; } Err(e) => { warn!( error = ?e, cache_key = %key, "Failed to serialize value for bulk caching" ); } } } debug!( items_count = success_count, total_items = items.len(), "Successfully bulk cached items in memory" ); Ok(()) } async fn ping(&mut self) -> Result { Ok(true) } async fn get_info(&mut self) -> Result { let data = self.data.read().await; let now = Instant::now(); let mut total_entries = 0; let mut expired_entries = 0; for (_, expiry) in data.values() { total_entries += 1; if let Some(exp) = expiry && *exp <= now { expired_entries += 1; } } Ok(format!( "InMemoryCache: {} total entries, {} expired, {} active", total_entries, expired_entries, total_entries - expired_entries )) } }