cachelito_core/cache_entry.rs
1use std::time::Instant;
2
3/// Internal wrapper that tracks when a value was inserted into the cache.
4/// Used for TTL expiration support.
5///
6/// This structure is used internally to support TTL (Time To Live) expiration.
7/// Each cached value is wrapped in a `CacheEntry` which records the insertion
8/// timestamp using `Instant::now()`.
9///
10/// # Type Parameters
11///
12/// * `R` - The type of the cached value
13///
14/// # Fields
15///
16/// * `value` - The actual cached value
17/// * `inserted_at` - The `Instant` when this entry was created
18/// * `frequency` - The number of times this entry has been accessed (for LFU policy)
19///
20/// # Examples
21///
22/// ```
23/// use cachelito_core::CacheEntry;
24///
25/// let entry = CacheEntry::new(42);
26/// assert_eq!(entry.value, 42);
27/// assert_eq!(entry.frequency, 0);
28///
29/// // Check if expired (TTL of 60 seconds)
30/// assert!(!entry.is_expired(Some(60)));
31/// ```
32#[derive(Clone)]
33pub struct CacheEntry<R> {
34 pub value: R,
35 pub inserted_at: Instant,
36 pub frequency: u64,
37}
38
39impl<R> CacheEntry<R> {
40 /// Creates a new cache entry with the current timestamp.
41 ///
42 /// # Arguments
43 ///
44 /// * `value` - The value to cache
45 ///
46 /// # Returns
47 ///
48 /// A new `CacheEntry` with `inserted_at` set to `Instant::now()` and `frequency` set to 0
49 pub fn new(value: R) -> Self {
50 Self {
51 value,
52 inserted_at: Instant::now(),
53 frequency: 0,
54 }
55 }
56
57 /// Returns true if the entry has expired based on the provided TTL.
58 ///
59 /// # Arguments
60 ///
61 /// * `ttl` - Optional time-to-live in seconds. `None` means no expiration.
62 ///
63 /// # Returns
64 ///
65 /// * `true` if the entry age exceeds the TTL
66 /// * `false` if TTL is `None` or the entry is still valid
67 ///
68 /// # Examples
69 ///
70 /// ```
71 /// use cachelito_core::CacheEntry;
72 /// use std::thread;
73 /// use std::time::Duration;
74 ///
75 /// let entry = CacheEntry::new("data");
76 ///
77 /// // Fresh entry is not expired
78 /// assert!(!entry.is_expired(Some(1)));
79 ///
80 /// // Wait 2 seconds
81 /// thread::sleep(Duration::from_secs(2));
82 ///
83 /// // Now it's expired (TTL was 1 second)
84 /// assert!(entry.is_expired(Some(1)));
85 ///
86 /// // No TTL means never expires
87 /// assert!(!entry.is_expired(None));
88 /// ```
89 pub fn is_expired(&self, ttl: Option<u64>) -> bool {
90 if let Some(ttl_secs) = ttl {
91 self.inserted_at.elapsed().as_secs() >= ttl_secs
92 } else {
93 false
94 }
95 }
96
97 /// Increments the access frequency counter.
98 ///
99 /// This method is used by the LFU (Least Frequently Used) eviction policy
100 /// to track how many times an entry has been accessed.
101 ///
102 /// # Examples
103 ///
104 /// ```
105 /// use cachelito_core::CacheEntry;
106 ///
107 /// let mut entry = CacheEntry::new(42);
108 /// assert_eq!(entry.frequency, 0);
109 ///
110 /// entry.increment_frequency();
111 /// assert_eq!(entry.frequency, 1);
112 ///
113 /// entry.increment_frequency();
114 /// assert_eq!(entry.frequency, 2);
115 /// ```
116 pub fn increment_frequency(&mut self) {
117 self.frequency = self.frequency.saturating_add(1);
118 }
119}
120
121// Implement MemoryEstimator for CacheEntry
122use crate::MemoryEstimator;
123
124impl<R: MemoryEstimator> MemoryEstimator for CacheEntry<R> {
125 fn estimate_memory(&self) -> usize {
126 // Base struct size (stack-allocated data: Instant + u64)
127 let base = std::mem::size_of::<Self>();
128
129 // Size of the value (including heap allocations)
130 let value_size = self.value.estimate_memory();
131
132 // Note: We subtract size_of_val(&self.value) to avoid double-counting
133 // the stack-allocated part which is already included in `base`
134 base + value_size.saturating_sub(std::mem::size_of_val(&self.value))
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::thread;
142 use std::time::Duration;
143
144 #[test]
145 fn test_new_entry_not_expired() {
146 let entry = CacheEntry::new(42);
147 assert_eq!(entry.value, 42);
148 assert!(!entry.is_expired(Some(10)));
149 }
150
151 #[test]
152 fn test_entry_expiration() {
153 let entry = CacheEntry::new("data");
154 thread::sleep(Duration::from_secs(2));
155 assert!(entry.is_expired(Some(1)));
156 assert!(!entry.is_expired(Some(3)));
157 }
158
159 #[test]
160 fn test_no_ttl_never_expires() {
161 let entry = CacheEntry::new(100);
162 thread::sleep(Duration::from_millis(100));
163 assert!(!entry.is_expired(None));
164 }
165
166 #[test]
167 fn test_memory_estimation_primitive() {
168 let entry = CacheEntry::new(42i32);
169 let estimated = entry.estimate_memory();
170
171 // Should be approximately: size_of::<CacheEntry<i32>>()
172 // (Instant + u64 + i32)
173 assert!(estimated >= std::mem::size_of::<CacheEntry<i32>>());
174 }
175
176 #[test]
177 fn test_memory_estimation_string() {
178 let s = String::from("Hello, World!");
179 let entry = CacheEntry::new(s.clone());
180 let estimated = entry.estimate_memory();
181
182 // Should include struct overhead + string capacity
183 let expected_min = std::mem::size_of::<CacheEntry<String>>() + s.capacity();
184 assert!(estimated >= expected_min);
185 }
186
187 #[test]
188 fn test_memory_estimation_vec() {
189 let v = vec![1, 2, 3, 4, 5];
190 let entry = CacheEntry::new(v.clone());
191 let estimated = entry.estimate_memory();
192
193 // Should include struct overhead + vec capacity
194 let expected_min =
195 std::mem::size_of::<CacheEntry<Vec<i32>>>() + v.capacity() * std::mem::size_of::<i32>();
196 assert!(estimated >= expected_min);
197 }
198}