Skip to main content

multi_tier_cache/backends/
memcached_cache.rs

1//! Memcached Cache - Distributed Cache Backend
2//!
3//! Memcached-based distributed cache for warm data storage with simple key-value operations.
4
5use crate::error::{CacheError, CacheResult};
6use bytes::Bytes;
7use futures_util::future::BoxFuture;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::Duration;
11use tracing::{debug, info};
12
13/// Memcached distributed cache
14///
15/// This is an alternative L2 (warm tier) cache backend, providing:
16/// - Distributed caching across multiple instances
17/// - Simple key-value storage (no persistence)
18/// - LRU eviction policy
19/// - High throughput for read-heavy workloads
20///
21/// **Note**: Unlike Redis, Memcached does not support:
22/// - TTL introspection (cannot get remaining TTL)
23/// - Persistence to disk
24/// - Advanced data structures
25pub struct MemcachedCache {
26    /// Memcached client
27    client: Arc<memcache::Client>,
28    /// Hit counter
29    hits: Arc<AtomicU64>,
30    /// Miss counter
31    misses: Arc<AtomicU64>,
32    /// Set counter
33    sets: Arc<AtomicU64>,
34}
35
36impl MemcachedCache {
37    /// Create new Memcached cache
38    ///
39    /// # Configuration
40    ///
41    /// Uses `MEMCACHED_URL` environment variable or defaults to `memcache://127.0.0.1:11211`
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if the Memcached client cannot be created.
46    pub fn new() -> CacheResult<Self> {
47        info!("Initializing Memcached Cache");
48
49        // Get Memcached URL from environment
50        let memcached_url = std::env::var("MEMCACHED_URL")
51            .unwrap_or_else(|_| "memcache://127.0.0.1:11211".to_string());
52
53        // Create Memcached client
54        let client = memcache::connect(memcached_url.as_str())
55            .map_err(|e| CacheError::ConfigError(format!("Failed to connect to Memcached: {e}")))?;
56
57        // Test connection with version command
58        match client.version() {
59            Ok(versions) => {
60                info!(
61                    url = %memcached_url,
62                    server_count = versions.len(),
63                    "Memcached Cache connected successfully"
64                );
65            }
66            Err(e) => {
67                return Err(CacheError::ConfigError(format!(
68                    "Memcached connection test failed: {e}"
69                )));
70            }
71        }
72
73        Ok(Self {
74            client: Arc::new(client),
75            hits: Arc::new(AtomicU64::new(0)),
76            misses: Arc::new(AtomicU64::new(0)),
77            sets: Arc::new(AtomicU64::new(0)),
78        })
79    }
80
81    /// Get cache statistics (from Memcached server)
82    ///
83    /// Returns server statistics like hits, misses, uptime, etc.
84    /// Each tuple contains (`server_address`, `stats_map`)
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if the stats cannot be retrieved.
89    pub fn get_server_stats(
90        &self,
91    ) -> CacheResult<Vec<(String, std::collections::HashMap<String, String>)>> {
92        self.client
93            .stats()
94            .map_err(|e| CacheError::BackendError(format!("Failed to get Memcached stats: {e}")))
95    }
96}
97
98// ===== Trait Implementations =====
99
100use crate::traits::CacheBackend;
101
102/// Implement `CacheBackend` trait for `MemcachedCache`
103impl CacheBackend for MemcachedCache {
104    fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Option<Bytes>> {
105        let client = Arc::clone(&self.client);
106        let key = key.to_string();
107        let hits = Arc::clone(&self.hits);
108        let misses = Arc::clone(&self.misses);
109        Box::pin(async move {
110            tokio::task::spawn_blocking(move || {
111                if let Ok(Some(bytes_vec)) = client.get::<Vec<u8>>(&key) {
112                    hits.fetch_add(1, Ordering::Relaxed);
113                    Some(Bytes::from(bytes_vec))
114                } else {
115                    misses.fetch_add(1, Ordering::Relaxed);
116                    None
117                }
118            })
119            .await
120            .unwrap_or(None)
121        })
122    }
123
124    fn set_with_ttl<'a>(
125        &'a self,
126        key: &'a str,
127        value: Bytes,
128        ttl: Duration,
129    ) -> BoxFuture<'a, CacheResult<()>> {
130        let client = Arc::clone(&self.client);
131        let key = key.to_string();
132        let sets = Arc::clone(&self.sets);
133        Box::pin(async move {
134            tokio::task::spawn_blocking(move || {
135                client
136                    .set(
137                        &key,
138                        value.as_ref(),
139                        u32::try_from(ttl.as_secs()).unwrap_or(u32::MAX),
140                    )
141                    .map_err(|e| {
142                        crate::error::CacheError::BackendError(format!(
143                            "Memcached operation failed: {e}"
144                        ))
145                    })?;
146
147                sets.fetch_add(1, Ordering::Relaxed);
148                debug!(key = %key, ttl_secs = %ttl.as_secs(), "[Memcached] Cached key with TTL");
149                Ok(())
150            })
151            .await
152            .map_err(|_| {
153                crate::error::CacheError::InternalError("Spawn blocking task failed".to_string())
154            })?
155        })
156    }
157
158    fn remove<'a>(&'a self, key: &'a str) -> BoxFuture<'a, CacheResult<()>> {
159        let client = Arc::clone(&self.client);
160        let key = key.to_string();
161        Box::pin(async move {
162            tokio::task::spawn_blocking(move || {
163                client.delete(&key).map_err(|e| {
164                    crate::error::CacheError::BackendError(format!(
165                        "Memcached operation failed: {e}"
166                    ))
167                })?;
168                Ok(())
169            })
170            .await
171            .map_err(|_| {
172                crate::error::CacheError::InternalError("Spawn blocking task failed".to_string())
173            })?
174        })
175    }
176
177    fn health_check(&self) -> BoxFuture<'_, bool> {
178        Box::pin(async move {
179            let test_key = "health_check_memcached";
180            let test_value = Bytes::from_static(b"health_check");
181
182            match self
183                .set_with_ttl(test_key, test_value.clone(), Duration::from_secs(10))
184                .await
185            {
186                Ok(()) => match self.get(test_key).await {
187                    Some(retrieved) => {
188                        let _ = self.remove(test_key).await;
189                        retrieved == test_value
190                    }
191                    None => false,
192                },
193                Err(_) => false,
194            }
195        })
196    }
197
198    fn name(&self) -> &'static str {
199        "Memcached"
200    }
201}
202
203impl crate::traits::L2CacheBackend for MemcachedCache {
204    fn get_with_ttl<'a>(
205        &'a self,
206        key: &'a str,
207    ) -> BoxFuture<'a, Option<(Bytes, Option<Duration>)>> {
208        Box::pin(async move { self.get(key).await.map(|v| (v, None)) })
209    }
210}