1use std::{
2 collections::{HashMap, VecDeque},
3 future::Future,
4 hash::Hash,
5 sync::{
6 atomic::{AtomicU64, Ordering},
7 Mutex,
8 },
9 time::{Duration, Instant},
10};
11
12#[cfg(any(
17 feature = "stores-sql",
18 feature = "stores-mongo",
19 feature = "stores-redis",
20 test
21))]
22pub(crate) fn jittered_ttl(base: Duration, jitter: Duration, sequence: u64) -> Duration {
23 if jitter.is_zero() {
24 return base;
25 }
26
27 let mut value = sequence.wrapping_add(0x9e37_79b9_7f4a_7c15);
29 value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
30 value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
31 value ^= value >> 31;
32
33 let jitter_nanos = jitter.as_nanos();
34 let added_nanos = u128::from(value) % (jitter_nanos.saturating_add(1));
35 let added = Duration::new(
36 (added_nanos / 1_000_000_000).min(u128::from(u64::MAX)) as u64,
37 (added_nanos % 1_000_000_000) as u32,
38 );
39 base.saturating_add(added)
40}
41
42use crate::{SingleFlight, SingleFlightError};
43
44struct Entry<V> {
45 value: V,
46 expires_at: Instant,
47}
48
49pub struct TtlCache<K, V> {
51 entries: Mutex<HashMap<K, Entry<V>>>,
52}
53
54impl<K, V> Default for TtlCache<K, V>
55where
56 K: Eq + Hash,
57{
58 fn default() -> Self {
59 Self {
60 entries: Mutex::new(HashMap::new()),
61 }
62 }
63}
64
65impl<K, V> TtlCache<K, V>
66where
67 K: Eq + Hash,
68{
69 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn insert(&self, key: K, value: V, ttl: Duration) {
74 assert!(!ttl.is_zero(), "cache TTL must be greater than zero");
75 self.entries.lock().expect("cache lock poisoned").insert(
76 key,
77 Entry {
78 value,
79 expires_at: Instant::now() + ttl,
80 },
81 );
82 }
83
84 pub fn get(&self, key: &K) -> Option<V>
85 where
86 V: Clone,
87 {
88 let mut entries = self.entries.lock().expect("cache lock poisoned");
89 let expired = entries
90 .get(key)
91 .is_some_and(|entry| entry.expires_at <= Instant::now());
92 if expired {
93 entries.remove(key);
94 None
95 } else {
96 entries.get(key).map(|entry| entry.value.clone())
97 }
98 }
99
100 pub fn remove(&self, key: &K) -> Option<V> {
101 self.entries
102 .lock()
103 .expect("cache lock poisoned")
104 .remove(key)
105 .map(|entry| entry.value)
106 }
107
108 pub fn len(&self) -> usize {
109 let now = Instant::now();
110 let mut entries = self.entries.lock().expect("cache lock poisoned");
111 entries.retain(|_, entry| entry.expires_at > now);
112 entries.len()
113 }
114
115 pub fn is_empty(&self) -> bool {
116 self.len() == 0
117 }
118}
119
120#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
122pub struct CacheStats {
123 pub hits: u64,
124 pub misses: u64,
125 pub insertions: u64,
126 pub evictions: u64,
127}
128
129#[derive(Default)]
130struct CacheCounters {
131 hits: AtomicU64,
132 misses: AtomicU64,
133 insertions: AtomicU64,
134 evictions: AtomicU64,
135}
136
137struct MemoryState<K, V> {
138 entries: HashMap<K, Entry<V>>,
139 least_to_most_recent: VecDeque<K>,
140}
141
142pub struct MemoryCache<K, V> {
148 capacity: usize,
149 state: Mutex<MemoryState<K, V>>,
150 counters: CacheCounters,
151}
152
153impl<K, V> MemoryCache<K, V>
154where
155 K: Clone + Eq + Hash,
156{
157 pub fn new(capacity: usize) -> Self {
158 assert!(capacity > 0, "cache capacity must be greater than zero");
159 Self {
160 capacity,
161 state: Mutex::new(MemoryState {
162 entries: HashMap::new(),
163 least_to_most_recent: VecDeque::new(),
164 }),
165 counters: CacheCounters::default(),
166 }
167 }
168
169 pub fn insert(&self, key: K, value: V, ttl: Duration) {
170 assert!(!ttl.is_zero(), "cache TTL must be greater than zero");
171 let mut state = self.state.lock().expect("cache lock poisoned");
172 remove_from_recency(&mut state.least_to_most_recent, &key);
173 state.least_to_most_recent.push_back(key.clone());
174 state.entries.insert(
175 key,
176 Entry {
177 value,
178 expires_at: Instant::now() + ttl,
179 },
180 );
181 self.counters.insertions.fetch_add(1, Ordering::Relaxed);
182
183 while state.entries.len() > self.capacity {
184 let Some(oldest) = state.least_to_most_recent.pop_front() else {
185 break;
186 };
187 if state.entries.remove(&oldest).is_some() {
188 self.counters.evictions.fetch_add(1, Ordering::Relaxed);
189 }
190 }
191 }
192
193 pub fn get(&self, key: &K) -> Option<V>
194 where
195 V: Clone,
196 {
197 let mut state = self.state.lock().expect("cache lock poisoned");
198 let now = Instant::now();
199 let expired = state
200 .entries
201 .get(key)
202 .is_some_and(|entry| entry.expires_at <= now);
203 if expired {
204 state.entries.remove(key);
205 remove_from_recency(&mut state.least_to_most_recent, key);
206 self.counters.evictions.fetch_add(1, Ordering::Relaxed);
207 self.counters.misses.fetch_add(1, Ordering::Relaxed);
208 return None;
209 }
210
211 let value = state.entries.get(key).map(|entry| entry.value.clone());
212 if value.is_some() {
213 remove_from_recency(&mut state.least_to_most_recent, key);
214 state.least_to_most_recent.push_back(key.clone());
215 self.counters.hits.fetch_add(1, Ordering::Relaxed);
216 } else {
217 self.counters.misses.fetch_add(1, Ordering::Relaxed);
218 }
219 value
220 }
221
222 pub fn remove(&self, key: &K) -> Option<V> {
223 let mut state = self.state.lock().expect("cache lock poisoned");
224 remove_from_recency(&mut state.least_to_most_recent, key);
225 state.entries.remove(key).map(|entry| entry.value)
226 }
227
228 pub fn remove_where<F>(&self, mut predicate: F) -> usize
233 where
234 F: FnMut(&K, &V) -> bool,
235 {
236 let mut state = self.state.lock().expect("cache lock poisoned");
237 let keys: Vec<_> = state
238 .entries
239 .iter()
240 .filter(|(key, entry)| predicate(key, &entry.value))
241 .map(|(key, _)| key.clone())
242 .collect();
243 for key in &keys {
244 state.entries.remove(key);
245 remove_from_recency(&mut state.least_to_most_recent, key);
246 }
247 keys.len()
248 }
249
250 pub fn clear(&self) {
251 let mut state = self.state.lock().expect("cache lock poisoned");
252 state.entries.clear();
253 state.least_to_most_recent.clear();
254 }
255
256 pub fn len(&self) -> usize {
257 let now = Instant::now();
258 let mut state = self.state.lock().expect("cache lock poisoned");
259 let expired: Vec<_> = state
260 .entries
261 .iter()
262 .filter(|(_, entry)| entry.expires_at <= now)
263 .map(|(key, _)| key.clone())
264 .collect();
265 for key in &expired {
266 state.entries.remove(key);
267 remove_from_recency(&mut state.least_to_most_recent, key);
268 }
269 self.counters
270 .evictions
271 .fetch_add(expired.len() as u64, Ordering::Relaxed);
272 state.entries.len()
273 }
274
275 pub fn is_empty(&self) -> bool {
276 self.len() == 0
277 }
278
279 pub fn stats(&self) -> CacheStats {
280 CacheStats {
281 hits: self.counters.hits.load(Ordering::Relaxed),
282 misses: self.counters.misses.load(Ordering::Relaxed),
283 insertions: self.counters.insertions.load(Ordering::Relaxed),
284 evictions: self.counters.evictions.load(Ordering::Relaxed),
285 }
286 }
287}
288
289fn remove_from_recency<K>(recency: &mut VecDeque<K>, key: &K)
290where
291 K: Eq,
292{
293 if let Some(index) = recency.iter().position(|candidate| candidate == key) {
294 recency.remove(index);
295 }
296}
297
298pub struct ReadThroughCache<K, V, E> {
300 cache: MemoryCache<K, V>,
301 flights: SingleFlight<K, V, E>,
302}
303
304impl<K, V, E> ReadThroughCache<K, V, E>
305where
306 K: Clone + Eq + Hash,
307 V: Clone,
308{
309 pub fn new(capacity: usize) -> Self {
310 Self {
311 cache: MemoryCache::new(capacity),
312 flights: SingleFlight::new(),
313 }
314 }
315
316 pub fn get(&self, key: &K) -> Option<V> {
317 self.cache.get(key)
318 }
319
320 pub async fn take<F, Fut>(
322 &self,
323 key: K,
324 ttl: Duration,
325 fetch: F,
326 ) -> Result<V, SingleFlightError<E>>
327 where
328 F: FnOnce() -> Fut,
329 Fut: Future<Output = Result<V, E>>,
330 {
331 if let Some(value) = self.cache.get(&key) {
332 return Ok(value);
333 }
334
335 self.flights
336 .execute(key.clone(), || async {
337 if let Some(value) = self.cache.get(&key) {
338 return Ok(value);
339 }
340 let value = fetch().await?;
341 self.cache.insert(key, value.clone(), ttl);
342 Ok(value)
343 })
344 .await
345 }
346
347 pub fn remove(&self, key: &K) -> Option<V> {
348 self.cache.remove(key)
349 }
350
351 pub fn stats(&self) -> CacheStats {
352 self.cache.stats()
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn expiry_jitter_is_bounded_and_varies_by_sequence() {
362 let base = Duration::from_secs(10);
363 let jitter = Duration::from_secs(5);
364 let values: Vec<_> = (0..8)
365 .map(|sequence| jittered_ttl(base, jitter, sequence))
366 .collect();
367
368 assert!(values
369 .iter()
370 .all(|ttl| *ttl >= base && *ttl <= base + jitter));
371 assert!(values.windows(2).any(|pair| pair[0] != pair[1]));
372 assert_eq!(jittered_ttl(base, Duration::ZERO, 99), base);
373 }
374 use std::thread;
375
376 #[test]
377 fn returns_a_value_until_its_ttl_expires() {
378 let cache = TtlCache::new();
379 cache.insert("user-42", "cached", Duration::from_millis(5));
380
381 assert_eq!(cache.get(&"user-42"), Some("cached"));
382 thread::sleep(Duration::from_millis(10));
383 assert_eq!(cache.get(&"user-42"), None);
384 assert!(cache.is_empty());
385 }
386
387 #[test]
388 fn remove_returns_the_cached_value() {
389 let cache = TtlCache::new();
390 cache.insert("user-42", 42, Duration::from_secs(1));
391
392 assert_eq!(cache.remove(&"user-42"), Some(42));
393 assert_eq!(cache.get(&"user-42"), None);
394 }
395
396 #[test]
397 fn bounded_cache_evicts_the_least_recently_used_value() {
398 let cache = MemoryCache::new(2);
399 cache.insert("one", 1, Duration::from_secs(1));
400 cache.insert("two", 2, Duration::from_secs(1));
401 assert_eq!(cache.get(&"one"), Some(1));
402 cache.insert("three", 3, Duration::from_secs(1));
403
404 assert_eq!(cache.get(&"two"), None);
405 assert_eq!(cache.get(&"one"), Some(1));
406 assert_eq!(cache.get(&"three"), Some(3));
407 assert_eq!(cache.stats().evictions, 1);
408 }
409
410 #[test]
411 fn bounded_cache_removes_matching_values_atomically() {
412 let cache = MemoryCache::new(4);
413 cache.insert("email", Some(1), Duration::from_secs(1));
414 cache.insert("phone", Some(1), Duration::from_secs(1));
415 cache.insert("other", Some(2), Duration::from_secs(1));
416 cache.insert("missing", None, Duration::from_secs(1));
417
418 assert_eq!(cache.remove_where(|_, value| *value == Some(1)), 2);
419 assert_eq!(cache.get(&"email"), None);
420 assert_eq!(cache.get(&"phone"), None);
421 assert_eq!(cache.get(&"other"), Some(Some(2)));
422 assert_eq!(cache.get(&"missing"), Some(None));
423 }
424
425 #[tokio::test]
426 async fn read_through_cache_fetches_and_reuses_a_value() {
427 use std::sync::atomic::{AtomicUsize, Ordering};
428
429 let cache = ReadThroughCache::<String, usize, String>::new(10);
430 let calls = AtomicUsize::new(0);
431 for _ in 0..2 {
432 let value = cache
433 .take("answer".to_owned(), Duration::from_secs(1), || async {
434 calls.fetch_add(1, Ordering::SeqCst);
435 Ok(42)
436 })
437 .await
438 .unwrap();
439 assert_eq!(value, 42);
440 }
441 assert_eq!(calls.load(Ordering::SeqCst), 1);
442 }
443}