multi_tier_cache/backends/
dashmap_cache.rs

1//! `DashMap` Cache - Simple Concurrent `HashMap` Backend
2//!
3//! A lightweight in-memory cache using `DashMap` for concurrent access.
4//! This is a reference implementation showing how to create custom cache backends.
5
6use anyhow::Result;
7use dashmap::DashMap;
8use serde_json;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12use tracing::{debug, info};
13
14/// Cache entry with expiration tracking
15#[derive(Debug, Clone)]
16struct CacheEntry {
17    value: serde_json::Value,
18    expires_at: Option<Instant>,
19}
20
21impl CacheEntry {
22    fn new(value: serde_json::Value, ttl: Duration) -> Self {
23        Self {
24            value,
25            expires_at: Some(Instant::now() + ttl),
26        }
27    }
28
29    fn is_expired(&self) -> bool {
30        self.expires_at
31            .is_some_and(|expires_at| Instant::now() > expires_at)
32    }
33}
34
35/// Simple concurrent cache using `DashMap`
36///
37/// **Use Case**: Educational reference, simple concurrent scenarios
38///
39/// **Features**:
40/// - Lock-free concurrent reads/writes
41/// - Manual TTL tracking
42/// - No automatic eviction (manual cleanup needed)
43/// - Minimal memory overhead
44///
45/// **Limitations**:
46/// - No automatic eviction policy (LRU, LFU, etc.)
47/// - No size limits (unbounded growth)
48/// - Manual TTL cleanup required
49///
50/// **When to use**:
51/// - Learning how to implement cache backends
52/// - Simple use cases with predictable data sizes
53/// - When you need full control over eviction logic
54///
55/// **Example**:
56/// ```rust
57/// use multi_tier_cache::backends::DashMapCache;
58/// use multi_tier_cache::traits::CacheBackend;
59/// use std::time::Duration;
60///
61/// # async fn example() -> anyhow::Result<()> {
62/// let cache = DashMapCache::new();
63/// let value = serde_json::json!({"user": "alice"});
64///
65/// cache.set_with_ttl("user:1", value.clone(), Duration::from_secs(60)).await?;
66/// let cached = cache.get("user:1").await;
67/// assert_eq!(cached, Some(value));
68/// # Ok(())
69/// # }
70/// ```
71pub struct DashMapCache {
72    /// Concurrent `HashMap`
73    map: Arc<DashMap<String, CacheEntry>>,
74    /// Hit counter
75    hits: Arc<AtomicU64>,
76    /// Miss counter
77    misses: Arc<AtomicU64>,
78    /// Set counter
79    sets: Arc<AtomicU64>,
80}
81
82impl DashMapCache {
83    /// Create new `DashMap` cache
84    pub fn new() -> Self {
85        info!("Initializing DashMap Cache (concurrent HashMap)");
86
87        Self {
88            map: Arc::new(DashMap::new()),
89            hits: Arc::new(AtomicU64::new(0)),
90            misses: Arc::new(AtomicU64::new(0)),
91            sets: Arc::new(AtomicU64::new(0)),
92        }
93    }
94
95    /// Cleanup expired entries (should be called periodically)
96    ///
97    /// **Note**: `DashMap` doesn't have automatic eviction, so you need to
98    /// call this method periodically to remove expired entries.
99    pub fn cleanup_expired(&self) -> usize {
100        let mut removed = 0;
101        self.map.retain(|_, entry| {
102            if entry.is_expired() {
103                removed += 1;
104                false // Remove
105            } else {
106                true // Keep
107            }
108        });
109        if removed > 0 {
110            debug!(count = removed, "[DashMap] Cleaned up expired entries");
111        }
112        removed
113    }
114
115    /// Get current cache size
116    #[must_use]
117    pub fn len(&self) -> usize {
118        self.map.len()
119    }
120
121    /// Check if cache is empty
122    #[must_use]
123    pub fn is_empty(&self) -> bool {
124        self.map.is_empty()
125    }
126}
127
128impl Default for DashMapCache {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134// ===== Trait Implementations =====
135
136use crate::traits::CacheBackend;
137use async_trait::async_trait;
138
139/// Implement `CacheBackend` trait for `DashMapCache`
140#[async_trait]
141impl CacheBackend for DashMapCache {
142    async fn get(&self, key: &str) -> Option<serde_json::Value> {
143        if let Some(entry) = self.map.get(key) {
144            if entry.is_expired() {
145                // Remove expired entry
146                drop(entry); // Release read lock
147                self.map.remove(key);
148                self.misses.fetch_add(1, Ordering::Relaxed);
149                None
150            } else {
151                self.hits.fetch_add(1, Ordering::Relaxed);
152                Some(entry.value.clone())
153            }
154        } else {
155            self.misses.fetch_add(1, Ordering::Relaxed);
156            None
157        }
158    }
159
160    async fn set_with_ttl(&self, key: &str, value: serde_json::Value, ttl: Duration) -> Result<()> {
161        let entry = CacheEntry::new(value, ttl);
162        self.map.insert(key.to_string(), entry);
163        self.sets.fetch_add(1, Ordering::Relaxed);
164        debug!(key = %key, ttl_secs = %ttl.as_secs(), "[DashMap] Cached key with TTL");
165        Ok(())
166    }
167
168    async fn remove(&self, key: &str) -> Result<()> {
169        self.map.remove(key);
170        Ok(())
171    }
172
173    async fn health_check(&self) -> bool {
174        let test_key = "health_check_dashmap";
175        let test_value = serde_json::json!({"test": true});
176
177        match self
178            .set_with_ttl(test_key, test_value.clone(), Duration::from_secs(60))
179            .await
180        {
181            Ok(()) => match self.get(test_key).await {
182                Some(retrieved) => {
183                    let _ = self.remove(test_key).await;
184                    retrieved == test_value
185                }
186                None => false,
187            },
188            Err(_) => false,
189        }
190    }
191
192    fn name(&self) -> &'static str {
193        "DashMap"
194    }
195}