in_memory_cache/
stats.rs

1//! Statistics and metrics for the cache.
2//!
3//! This module provides atomic counters for tracking cache operations,
4//! enabling observability without impacting performance.
5
6use std::sync::atomic::{AtomicU64, Ordering};
7
8/// Statistics for cache operations.
9///
10/// All counters are atomic and can be safely accessed from multiple threads.
11/// Use `Cache::stats()` to get a snapshot of the current statistics.
12///
13/// # Example
14/// ```ignore
15/// let cache = Cache::new(CacheConfig::default());
16/// cache.set("key", "value");
17/// let stats = cache.stats();
18/// println!("Cache size: {}", stats.size());
19/// ```
20#[derive(Debug, Default)]
21pub struct CacheStats {
22    /// Number of successful get operations (key found).
23    hits: AtomicU64,
24
25    /// Number of failed get operations (key not found or expired).
26    misses: AtomicU64,
27
28    /// Number of entries evicted due to capacity limits.
29    evictions: AtomicU64,
30
31    /// Number of entries removed due to TTL expiration.
32    expirations: AtomicU64,
33
34    /// Current number of entries in the cache.
35    size: AtomicU64,
36
37    /// Total number of set operations performed.
38    sets: AtomicU64,
39
40    /// Total number of delete operations performed.
41    deletes: AtomicU64,
42}
43
44impl CacheStats {
45    /// Create a new stats instance with all counters at zero.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Record a cache hit.
51    pub fn record_hit(&self) {
52        self.hits.fetch_add(1, Ordering::Relaxed);
53    }
54
55    /// Record a cache miss.
56    pub fn record_miss(&self) {
57        self.misses.fetch_add(1, Ordering::Relaxed);
58    }
59
60    /// Record an eviction (due to capacity).
61    pub fn record_eviction(&self) {
62        self.evictions.fetch_add(1, Ordering::Relaxed);
63    }
64
65    /// Record an expiration (due to TTL).
66    pub fn record_expiration(&self) {
67        self.expirations.fetch_add(1, Ordering::Relaxed);
68    }
69
70    /// Record a set operation.
71    pub fn record_set(&self) {
72        self.sets.fetch_add(1, Ordering::Relaxed);
73    }
74
75    /// Record a delete operation.
76    pub fn record_delete(&self) {
77        self.deletes.fetch_add(1, Ordering::Relaxed);
78    }
79
80    /// Increment the size counter.
81    pub fn increment_size(&self) {
82        self.size.fetch_add(1, Ordering::Relaxed);
83    }
84
85    /// Decrement the size counter.
86    pub fn decrement_size(&self) {
87        self.size.fetch_sub(1, Ordering::Relaxed);
88    }
89
90    /// Set the size to a specific value.
91    pub fn set_size(&self, size: u64) {
92        self.size.store(size, Ordering::Relaxed);
93    }
94
95    // Getters for reading statistics
96
97    /// Get the number of cache hits.
98    pub fn hits(&self) -> u64 {
99        self.hits.load(Ordering::Relaxed)
100    }
101
102    /// Get the number of cache misses.
103    pub fn misses(&self) -> u64 {
104        self.misses.load(Ordering::Relaxed)
105    }
106
107    /// Get the number of evictions.
108    pub fn evictions(&self) -> u64 {
109        self.evictions.load(Ordering::Relaxed)
110    }
111
112    /// Get the number of expirations.
113    pub fn expirations(&self) -> u64 {
114        self.expirations.load(Ordering::Relaxed)
115    }
116
117    /// Get the current cache size.
118    pub fn size(&self) -> u64 {
119        self.size.load(Ordering::Relaxed)
120    }
121
122    /// Get the total number of set operations.
123    pub fn sets(&self) -> u64 {
124        self.sets.load(Ordering::Relaxed)
125    }
126
127    /// Get the total number of delete operations.
128    pub fn deletes(&self) -> u64 {
129        self.deletes.load(Ordering::Relaxed)
130    }
131
132    /// Calculate the hit rate as a percentage (0.0 to 100.0).
133    /// Returns 0.0 if no operations have been performed.
134    pub fn hit_rate(&self) -> f64 {
135        let hits = self.hits();
136        let misses = self.misses();
137        let total = hits + misses;
138        if total == 0 {
139            0.0
140        } else {
141            (hits as f64 / total as f64) * 100.0
142        }
143    }
144
145    /// Create a snapshot of the current statistics.
146    /// This is useful for serialization or logging.
147    pub fn snapshot(&self) -> StatsSnapshot {
148        StatsSnapshot {
149            hits: self.hits(),
150            misses: self.misses(),
151            evictions: self.evictions(),
152            expirations: self.expirations(),
153            size: self.size(),
154            sets: self.sets(),
155            deletes: self.deletes(),
156            hit_rate: self.hit_rate(),
157        }
158    }
159}
160
161/// A point-in-time snapshot of cache statistics.
162///
163/// Unlike `CacheStats`, this struct contains plain values (not atomics)
164/// and can be easily serialized or logged.
165#[derive(Debug, Clone, PartialEq)]
166pub struct StatsSnapshot {
167    pub hits: u64,
168    pub misses: u64,
169    pub evictions: u64,
170    pub expirations: u64,
171    pub size: u64,
172    pub sets: u64,
173    pub deletes: u64,
174    pub hit_rate: f64,
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_initial_stats() {
183        let stats = CacheStats::new();
184        assert_eq!(stats.hits(), 0);
185        assert_eq!(stats.misses(), 0);
186        assert_eq!(stats.size(), 0);
187    }
188
189    #[test]
190    fn test_record_operations() {
191        let stats = CacheStats::new();
192
193        stats.record_hit();
194        stats.record_hit();
195        stats.record_miss();
196
197        assert_eq!(stats.hits(), 2);
198        assert_eq!(stats.misses(), 1);
199    }
200
201    #[test]
202    fn test_hit_rate() {
203        let stats = CacheStats::new();
204
205        // No operations = 0% hit rate
206        assert_eq!(stats.hit_rate(), 0.0);
207
208        // 3 hits, 1 miss = 75% hit rate
209        stats.record_hit();
210        stats.record_hit();
211        stats.record_hit();
212        stats.record_miss();
213
214        assert!((stats.hit_rate() - 75.0).abs() < 0.01);
215    }
216
217    #[test]
218    fn test_size_tracking() {
219        let stats = CacheStats::new();
220
221        stats.increment_size();
222        stats.increment_size();
223        assert_eq!(stats.size(), 2);
224
225        stats.decrement_size();
226        assert_eq!(stats.size(), 1);
227    }
228
229    #[test]
230    fn test_snapshot() {
231        let stats = CacheStats::new();
232        stats.record_hit();
233        stats.record_set();
234        stats.increment_size();
235
236        let snapshot = stats.snapshot();
237        assert_eq!(snapshot.hits, 1);
238        assert_eq!(snapshot.sets, 1);
239        assert_eq!(snapshot.size, 1);
240    }
241}