1use crate::{GitObject, ObjectId, Result};
7use lru::LruCache;
8use parking_lot::Mutex;
9use std::num::NonZeroUsize;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12#[derive(Debug, Clone)]
14pub struct CacheConfig {
15 pub max_objects: usize,
17 pub max_size_bytes: usize,
19 pub write_through: bool,
21 pub ttl_seconds: u64,
23}
24
25impl Default for CacheConfig {
26 fn default() -> Self {
27 Self {
28 max_objects: 10_000,
29 max_size_bytes: 256 * 1024 * 1024, write_through: true,
31 ttl_seconds: 0,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Default)]
38pub struct CacheStats {
39 pub hits: u64,
41 pub misses: u64,
43 pub evictions: u64,
45 pub size: usize,
47 pub memory_bytes: usize,
49}
50
51impl CacheStats {
52 pub fn hit_ratio(&self) -> f64 {
54 let total = self.hits + self.misses;
55 if total == 0 {
56 0.0
57 } else {
58 self.hits as f64 / total as f64
59 }
60 }
61}
62
63#[derive(Debug, Default)]
65pub struct CacheMetrics {
66 hits: AtomicU64,
67 misses: AtomicU64,
68 evictions: AtomicU64,
69}
70
71impl CacheMetrics {
72 pub fn new() -> Self {
74 Self::default()
75 }
76
77 pub fn record_hit(&self) {
79 self.hits.fetch_add(1, Ordering::Relaxed);
80 }
81
82 pub fn record_miss(&self) {
84 self.misses.fetch_add(1, Ordering::Relaxed);
85 }
86
87 pub fn record_eviction(&self) {
89 self.evictions.fetch_add(1, Ordering::Relaxed);
90 }
91
92 pub fn snapshot(&self) -> CacheStats {
94 CacheStats {
95 hits: self.hits.load(Ordering::Relaxed),
96 misses: self.misses.load(Ordering::Relaxed),
97 evictions: self.evictions.load(Ordering::Relaxed),
98 size: 0,
99 memory_bytes: 0,
100 }
101 }
102}
103
104pub struct CachedStorage<S> {
108 inner: S,
110 cache: Mutex<LruCache<ObjectId, GitObject>>,
112 current_size: AtomicU64,
114 config: CacheConfig,
116 metrics: CacheMetrics,
118}
119
120impl<S> CachedStorage<S> {
121 pub fn new(inner: S, config: CacheConfig) -> Self {
123 let max_objects = NonZeroUsize::new(config.max_objects).unwrap_or(NonZeroUsize::MIN);
124 Self {
125 inner,
126 cache: Mutex::new(LruCache::new(max_objects)),
127 current_size: AtomicU64::new(0),
128 config,
129 metrics: CacheMetrics::new(),
130 }
131 }
132
133 pub fn with_defaults(inner: S) -> Self {
135 Self::new(inner, CacheConfig::default())
136 }
137
138 pub fn inner(&self) -> &S {
140 &self.inner
141 }
142
143 pub fn config(&self) -> &CacheConfig {
145 &self.config
146 }
147
148 pub fn stats(&self) -> CacheStats {
150 let cache = self.cache.lock();
151 let mut stats = self.metrics.snapshot();
152 stats.size = cache.len();
153 stats.memory_bytes = self.current_size.load(Ordering::Relaxed) as usize;
154 stats
155 }
156
157 pub fn clear(&self) {
159 let mut cache = self.cache.lock();
160 cache.clear();
161 self.current_size.store(0, Ordering::Relaxed);
162 }
163
164 pub fn invalidate(&self, id: &ObjectId) {
166 let mut cache = self.cache.lock();
167 if let Some(obj) = cache.pop(id) {
168 let size = obj.data.len() as u64;
169 self.current_size.fetch_sub(size, Ordering::Relaxed);
170 }
171 }
172
173 fn cache_get(&self, id: &ObjectId) -> Option<GitObject> {
175 let mut cache = self.cache.lock();
176 cache.get(id).cloned()
177 }
178
179 fn cache_put(&self, object: &GitObject) {
181 let size = object.data.len() as u64;
182
183 let mut cache = self.cache.lock();
185 while self.current_size.load(Ordering::Relaxed) + size > self.config.max_size_bytes as u64 {
186 if let Some((_, evicted)) = cache.pop_lru() {
187 let evicted_size = evicted.data.len() as u64;
188 self.current_size.fetch_sub(evicted_size, Ordering::Relaxed);
189 self.metrics.record_eviction();
190 } else {
191 break;
192 }
193 }
194
195 if let Some(old) = cache.put(object.id, object.clone()) {
197 let old_size = old.data.len() as u64;
198 self.current_size.fetch_sub(old_size, Ordering::Relaxed);
199 }
200 self.current_size.fetch_add(size, Ordering::Relaxed);
201 }
202}
203
204impl<S> CachedStorage<S>
205where
206 S: crate::traits::ObjectStoreBackend,
207{
208 pub fn get(&self, id: &ObjectId) -> Result<Option<GitObject>> {
210 if let Some(obj) = self.cache_get(id) {
212 self.metrics.record_hit();
213 return Ok(Some(obj));
214 }
215
216 self.metrics.record_miss();
217
218 let result = self.inner.get(id)?;
220
221 if let Some(ref obj) = result {
223 self.cache_put(obj);
224 }
225
226 Ok(result)
227 }
228
229 pub fn put(&self, object: GitObject) -> Result<ObjectId> {
231 let id = self.inner.put(object.clone())?;
232
233 if self.config.write_through {
234 self.cache_put(&object);
235 }
236
237 Ok(id)
238 }
239
240 pub fn contains(&self, id: &ObjectId) -> Result<bool> {
242 {
244 let cache = self.cache.lock();
245 if cache.contains(id) {
246 return Ok(true);
247 }
248 }
249
250 self.inner.contains(id)
252 }
253
254 pub fn delete(&self, id: &ObjectId) -> Result<bool> {
256 self.invalidate(id);
257 self.inner.delete(id)
258 }
259
260 pub fn len(&self) -> Result<usize> {
262 self.inner.len()
263 }
264
265 pub fn is_empty(&self) -> Result<bool> {
267 self.inner.is_empty()
268 }
269
270 pub fn list_objects(&self) -> Result<Vec<ObjectId>> {
272 self.inner.list_objects()
273 }
274
275 pub fn batch_get(&self, ids: &[ObjectId]) -> Result<Vec<Option<GitObject>>> {
277 let mut results = Vec::with_capacity(ids.len());
278 let mut uncached = Vec::new();
279 let mut uncached_indices = Vec::new();
280
281 for (i, id) in ids.iter().enumerate() {
283 if let Some(obj) = self.cache_get(id) {
284 self.metrics.record_hit();
285 results.push(Some(obj));
286 } else {
287 self.metrics.record_miss();
288 uncached.push(*id);
289 uncached_indices.push(i);
290 results.push(None);
291 }
292 }
293
294 if !uncached.is_empty() {
296 let fetched = self.inner.batch_get(&uncached)?;
297 for (i, obj) in uncached_indices.into_iter().zip(fetched) {
298 if let Some(ref o) = obj {
299 self.cache_put(o);
300 }
301 results[i] = obj;
302 }
303 }
304
305 Ok(results)
306 }
307}
308
309impl<S> crate::traits::ObjectStoreBackend for CachedStorage<S>
311where
312 S: crate::traits::ObjectStoreBackend,
313{
314 fn put(&self, object: GitObject) -> Result<ObjectId> {
315 CachedStorage::put(self, object)
316 }
317
318 fn get(&self, id: &ObjectId) -> Result<Option<GitObject>> {
319 CachedStorage::get(self, id)
320 }
321
322 fn contains(&self, id: &ObjectId) -> Result<bool> {
323 CachedStorage::contains(self, id)
324 }
325
326 fn delete(&self, id: &ObjectId) -> Result<bool> {
327 CachedStorage::delete(self, id)
328 }
329
330 fn len(&self) -> Result<usize> {
331 CachedStorage::len(self)
332 }
333
334 fn list_objects(&self) -> Result<Vec<ObjectId>> {
335 CachedStorage::list_objects(self)
336 }
337
338 fn batch_get(&self, ids: &[ObjectId]) -> Result<Vec<Option<GitObject>>> {
339 CachedStorage::batch_get(self, ids)
340 }
341
342 fn flush(&self) -> Result<()> {
343 self.inner.flush()
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::ObjectStore;
351
352 #[test]
353 fn test_cache_hit() {
354 let store = ObjectStore::new();
355 let cached = CachedStorage::with_defaults(store);
356
357 let obj = GitObject::blob(b"test data".to_vec());
358 let id = cached.put(obj).unwrap();
359
360 let result = cached.get(&id).unwrap();
362 assert!(result.is_some());
363
364 let stats = cached.stats();
365 assert_eq!(stats.hits, 1);
366 assert_eq!(stats.misses, 0);
367 }
368
369 #[test]
370 fn test_cache_miss_then_hit() {
371 let store = ObjectStore::new();
372
373 let obj = GitObject::blob(b"test data".to_vec());
375 let id = store.put(obj);
376
377 let cached = CachedStorage::with_defaults(store);
378
379 let result = cached.get(&id).unwrap();
381 assert!(result.is_some());
382
383 let result = cached.get(&id).unwrap();
385 assert!(result.is_some());
386
387 let stats = cached.stats();
388 assert_eq!(stats.hits, 1);
389 assert_eq!(stats.misses, 1);
390 }
391
392 #[test]
393 fn test_cache_invalidation() {
394 let store = ObjectStore::new();
395 let cached = CachedStorage::with_defaults(store);
396
397 let obj = GitObject::blob(b"test data".to_vec());
398 let id = cached.put(obj).unwrap();
399
400 cached.invalidate(&id);
402
403 let _ = cached.get(&id).unwrap();
405
406 let stats = cached.stats();
407 assert_eq!(stats.misses, 1);
408 }
409
410 #[test]
411 fn test_cache_eviction() {
412 let config = CacheConfig {
413 max_objects: 2,
414 max_size_bytes: 50, write_through: true,
416 ttl_seconds: 0,
417 };
418 let store = ObjectStore::new();
419 let cached = CachedStorage::new(store, config);
420
421 for i in 0..3 {
423 let obj = GitObject::blob(format!("data-{}-padding", i).into_bytes());
424 cached.put(obj).unwrap();
425 }
426
427 let stats = cached.stats();
428 assert!(stats.size <= 2); }
430
431 #[test]
432 fn test_cache_clear() {
433 let store = ObjectStore::new();
434 let cached = CachedStorage::with_defaults(store);
435
436 let obj = GitObject::blob(b"test data".to_vec());
437 cached.put(obj).unwrap();
438
439 cached.clear();
440
441 let stats = cached.stats();
442 assert_eq!(stats.size, 0);
443 }
444
445 #[test]
446 fn test_hit_ratio() {
447 let stats = CacheStats {
448 hits: 8,
449 misses: 2,
450 evictions: 0,
451 size: 10,
452 memory_bytes: 1000,
453 };
454 assert!((stats.hit_ratio() - 0.8).abs() < 0.001);
455 }
456
457 #[test]
458 fn test_hit_ratio_zero_total() {
459 let stats = CacheStats::default();
460 assert_eq!(stats.hit_ratio(), 0.0);
461 }
462
463 impl crate::traits::ObjectStoreBackend for ObjectStore {
465 fn put(&self, object: GitObject) -> Result<ObjectId> {
466 Ok(ObjectStore::put(self, object))
467 }
468
469 fn get(&self, id: &ObjectId) -> Result<Option<GitObject>> {
470 match ObjectStore::get(self, id) {
471 Ok(obj) => Ok(Some(obj)),
472 Err(crate::StorageError::ObjectNotFound(_)) => Ok(None),
473 Err(e) => Err(e),
474 }
475 }
476
477 fn contains(&self, id: &ObjectId) -> Result<bool> {
478 Ok(ObjectStore::contains(self, id))
479 }
480
481 fn delete(&self, _id: &ObjectId) -> Result<bool> {
482 Ok(false) }
484
485 fn len(&self) -> Result<usize> {
486 Ok(ObjectStore::len(self))
487 }
488
489 fn list_objects(&self) -> Result<Vec<ObjectId>> {
490 Ok(ObjectStore::list_objects(self))
491 }
492 }
493}