1use pingora_memory_cache::MemoryCache;
12use std::hash::Hash;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::Arc;
15use std::time::Duration;
16use tracing::{debug, trace};
17
18#[derive(Debug, Clone)]
20pub struct MemoryCacheConfig {
21 pub max_items: usize,
23 pub default_ttl: Duration,
25 pub enable_stats: bool,
27}
28
29impl Default for MemoryCacheConfig {
30 fn default() -> Self {
31 Self {
32 max_items: 10_000,
33 default_ttl: Duration::from_secs(60),
34 enable_stats: true,
35 }
36 }
37}
38
39#[derive(Debug, Default)]
41pub struct MemoryCacheStats {
42 pub hits: AtomicU64,
44 pub misses: AtomicU64,
46 pub insertions: AtomicU64,
48 pub evictions: AtomicU64,
50}
51
52impl MemoryCacheStats {
53 pub fn hit_rate(&self) -> f64 {
55 let hits = self.hits.load(Ordering::Relaxed) as f64;
56 let misses = self.misses.load(Ordering::Relaxed) as f64;
57 let total = hits + misses;
58 if total > 0.0 {
59 (hits / total) * 100.0
60 } else {
61 0.0
62 }
63 }
64
65 pub fn reset(&self) {
67 self.hits.store(0, Ordering::Relaxed);
68 self.misses.store(0, Ordering::Relaxed);
69 self.insertions.store(0, Ordering::Relaxed);
70 self.evictions.store(0, Ordering::Relaxed);
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct RouteMatchEntry {
77 pub route_id: String,
79 pub upstream_id: Option<String>,
81 pub cached_at: std::time::Instant,
83}
84
85pub struct MemoryCacheManager {
92 route_cache: MemoryCache<String, RouteMatchEntry>,
94 config: MemoryCacheConfig,
96 stats: Arc<MemoryCacheStats>,
98}
99
100impl MemoryCacheManager {
101 pub fn new(config: MemoryCacheConfig) -> Self {
103 debug!(
104 max_items = config.max_items,
105 default_ttl_secs = config.default_ttl.as_secs(),
106 "Creating memory cache manager"
107 );
108
109 let estimated_item_size = 200;
112 let cache_size = config.max_items * estimated_item_size;
113
114 let route_cache = MemoryCache::new(cache_size);
115
116 Self {
117 route_cache,
118 config,
119 stats: Arc::new(MemoryCacheStats::default()),
120 }
121 }
122
123 pub fn get_route_match(&self, key: &str) -> Option<RouteMatchEntry> {
125 let key_string = key.to_string();
126 let (result, _status) = self.route_cache.get(&key_string);
127
128 if self.config.enable_stats {
129 if result.is_some() {
130 self.stats.hits.fetch_add(1, Ordering::Relaxed);
131 trace!(key = %key, "Route cache hit");
132 } else {
133 self.stats.misses.fetch_add(1, Ordering::Relaxed);
134 trace!(key = %key, "Route cache miss");
135 }
136 }
137
138 result
139 }
140
141 pub fn put_route_match(&self, key: &str, entry: RouteMatchEntry) {
143 self.put_route_match_with_ttl(key, entry, self.config.default_ttl);
144 }
145
146 pub fn put_route_match_with_ttl(&self, key: &str, entry: RouteMatchEntry, ttl: Duration) {
148 trace!(
149 key = %key,
150 route_id = %entry.route_id,
151 ttl_secs = ttl.as_secs(),
152 "Caching route match"
153 );
154
155 let key_string = key.to_string();
156 self.route_cache.put(&key_string, entry, Some(ttl));
157
158 if self.config.enable_stats {
159 self.stats.insertions.fetch_add(1, Ordering::Relaxed);
160 }
161 }
162
163 pub fn route_cache_key(method: &str, path: &str, host: Option<&str>) -> String {
170 match host {
171 Some(h) => format!("{}:{}:{}", method, h, path),
172 None => format!("{}:{}", method, path),
173 }
174 }
175
176 pub fn invalidate_route(&self, _route_id: &str) {
182 debug!("Route invalidation requested (requires cache clear)");
185 }
186
187 pub fn clear(&self) {
191 debug!("Clearing memory cache");
192 }
195
196 pub fn stats(&self) -> &MemoryCacheStats {
198 &self.stats
199 }
200
201 pub fn config(&self) -> &MemoryCacheConfig {
203 &self.config
204 }
205}
206
207pub struct TypedCache<K, V>
213where
214 K: Hash + Eq + Clone + Send + Sync + 'static,
215 V: Clone + Send + Sync + 'static,
216{
217 inner: MemoryCache<K, V>,
218 stats: Arc<MemoryCacheStats>,
219 default_ttl: Duration,
220}
221
222impl<K, V> TypedCache<K, V>
223where
224 K: Hash + Eq + Clone + Send + Sync + 'static,
225 V: Clone + Send + Sync + 'static,
226{
227 pub fn new(max_size_bytes: usize, default_ttl: Duration) -> Self {
229 Self {
230 inner: MemoryCache::new(max_size_bytes),
231 stats: Arc::new(MemoryCacheStats::default()),
232 default_ttl,
233 }
234 }
235
236 pub fn get(&self, key: &K) -> Option<V> {
238 let (result, _status) = self.inner.get(key);
239 if result.is_some() {
240 self.stats.hits.fetch_add(1, Ordering::Relaxed);
241 } else {
242 self.stats.misses.fetch_add(1, Ordering::Relaxed);
243 }
244 result
245 }
246
247 pub fn put(&self, key: &K, value: V) {
249 self.put_with_ttl(key, value, self.default_ttl);
250 }
251
252 pub fn put_with_ttl(&self, key: &K, value: V, ttl: Duration) {
254 self.inner.put(key, value, Some(ttl));
255 self.stats.insertions.fetch_add(1, Ordering::Relaxed);
256 }
257
258 pub fn stats(&self) -> &MemoryCacheStats {
260 &self.stats
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_route_cache_key() {
270 let key1 = MemoryCacheManager::route_cache_key("GET", "/api/users", Some("example.com"));
271 assert_eq!(key1, "GET:example.com:/api/users");
272
273 let key2 = MemoryCacheManager::route_cache_key("POST", "/api/data", None);
274 assert_eq!(key2, "POST:/api/data");
275 }
276
277 #[test]
278 fn test_memory_cache_basic() {
279 let config = MemoryCacheConfig::default();
280 let cache = MemoryCacheManager::new(config);
281
282 assert!(cache.get_route_match("test-key").is_none());
284 assert_eq!(cache.stats().misses.load(Ordering::Relaxed), 1);
285
286 let entry = RouteMatchEntry {
288 route_id: "route-1".to_string(),
289 upstream_id: Some("upstream-1".to_string()),
290 cached_at: std::time::Instant::now(),
291 };
292 cache.put_route_match("test-key", entry);
293
294 let result = cache.get_route_match("test-key");
296 assert!(result.is_some());
297 assert_eq!(result.unwrap().route_id, "route-1");
298 assert_eq!(cache.stats().hits.load(Ordering::Relaxed), 1);
299 }
300
301 #[test]
302 fn test_hit_rate() {
303 let stats = MemoryCacheStats::default();
304 stats.hits.store(80, Ordering::Relaxed);
305 stats.misses.store(20, Ordering::Relaxed);
306 assert!((stats.hit_rate() - 80.0).abs() < 0.001);
307 }
308
309 #[test]
310 fn test_typed_cache() {
311 let cache: TypedCache<String, String> =
312 TypedCache::new(1024 * 1024, Duration::from_secs(60));
313
314 let key = "key1".to_string();
315 cache.put(&key, "value1".to_string());
316 let result = cache.get(&key);
317 assert_eq!(result, Some("value1".to_string()));
318 }
319}