1use bytes::Bytes;
2use quick_cache::sync::{Cache, DefaultLifecycle};
3use quick_cache::{OptionsBuilder, UnitWeighter};
4use std::hash::RandomState;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8#[derive(Clone)]
9struct L1Entry {
10 data: Bytes,
13 ttl: Duration,
14 created_at: Instant,
15 expires_at: Option<Instant>,
19 freshness_jitter: f64,
20}
21
22impl L1Entry {
23 fn is_live(&self, now: Instant) -> bool {
24 self.expires_at.is_none_or(|deadline| now < deadline)
25 }
26}
27
28type Store = Cache<String, L1Entry, UnitWeighter, RandomState, DefaultLifecycle<String, L1Entry>>;
34
35const MIN_SHARD_ENTRIES: usize = 128;
41
42const MAX_SHARDS: usize = 64;
44
45fn shard_count(capacity: usize) -> usize {
49 1 << (capacity / MIN_SHARD_ENTRIES).clamp(1, MAX_SHARDS).ilog2()
50}
51
52#[derive(Debug, Clone, PartialEq)]
54pub enum L1SwrRead {
55 Fresh(Vec<u8>),
57 Stale(Vec<u8>),
61 Miss,
63}
64
65pub(crate) enum SharedSwrRead {
67 Fresh(Bytes),
68 Stale(Bytes),
69 Miss,
70}
71
72#[derive(Clone)]
88pub struct L1Cache {
89 store: Arc<Store>,
90}
91
92impl L1Cache {
93 pub fn new(capacity: usize) -> Self {
95 let shards = shard_count(capacity);
96 let weight = u64::try_from(capacity / shards * shards).unwrap_or(u64::MAX);
99 #[allow(clippy::expect_used)]
103 let options = OptionsBuilder::new()
104 .shards(shards)
105 .estimated_items_capacity(capacity)
106 .weight_capacity(weight)
107 .build()
108 .expect("L1 options set both capacities");
109 Self {
110 store: Arc::new(Store::with_options(
111 options,
112 UnitWeighter,
113 RandomState::new(),
114 DefaultLifecycle::default(),
115 )),
116 }
117 }
118
119 pub fn get(&self, key: &str) -> Option<Vec<u8>> {
121 self.get_shared(key).map(|data| data.to_vec())
122 }
123
124 pub(crate) fn get_shared(&self, key: &str) -> Option<Bytes> {
126 let entry = self.store.get(key)?;
127 let now = Instant::now();
128 if entry.is_live(now) {
129 return Some(entry.data);
130 }
131 self.remove_expired(key, now);
132 None
133 }
134
135 pub(crate) fn contains(&self, key: &str) -> bool {
139 let Some(entry) = self.store.peek(key) else {
140 return false;
141 };
142 let now = Instant::now();
143 if entry.is_live(now) {
144 return true;
145 }
146 self.remove_expired(key, now);
147 false
148 }
149
150 pub fn get_with_swr(&self, key: &str, threshold_ratio: f64) -> L1SwrRead {
169 match self.get_with_swr_shared(key, threshold_ratio) {
170 SharedSwrRead::Fresh(data) => L1SwrRead::Fresh(data.to_vec()),
171 SharedSwrRead::Stale(data) => L1SwrRead::Stale(data.to_vec()),
172 SharedSwrRead::Miss => L1SwrRead::Miss,
173 }
174 }
175
176 pub(crate) fn get_with_swr_shared(&self, key: &str, threshold_ratio: f64) -> SharedSwrRead {
178 let Some(entry) = self.store.get(key) else {
179 return SharedSwrRead::Miss;
180 };
181 let now = Instant::now();
182 if !entry.is_live(now) {
183 self.remove_expired(key, now);
184 return SharedSwrRead::Miss;
185 }
186 let threshold = entry.ttl.as_secs_f64() * threshold_ratio * entry.freshness_jitter;
187 let age = now.saturating_duration_since(entry.created_at);
188 if age.as_secs_f64() > threshold {
189 SharedSwrRead::Stale(entry.data)
190 } else {
191 SharedSwrRead::Fresh(entry.data)
192 }
193 }
194
195 pub fn set(&self, key: &str, value: &[u8], ttl: Duration) {
199 self.set_shared(key, Bytes::copy_from_slice(value), ttl);
200 }
201
202 pub(crate) fn set_shared(&self, key: &str, value: Bytes, ttl: Duration) {
204 let created_at = Instant::now();
205 self.store.insert(
206 key.to_owned(),
207 L1Entry {
208 data: value,
209 ttl,
210 created_at,
211 expires_at: created_at.checked_add(ttl),
212 freshness_jitter: 0.9 + crate::random_unit() * 0.2,
213 },
214 );
215 }
216
217 pub fn delete(&self, key: &str) {
219 self.store.remove(key);
220 }
221
222 pub fn entry_count(&self) -> u64 {
228 self.run_pending_tasks();
229 u64::try_from(self.store.len()).unwrap_or(u64::MAX)
230 }
231
232 pub fn run_pending_tasks(&self) {
238 let now = Instant::now();
239 self.store.retain(|_, entry| entry.is_live(now));
240 }
241
242 fn remove_expired(&self, key: &str, now: Instant) {
249 self.store.remove_if(key, |current| !current.is_live(now));
250 }
251}
252
253#[cfg(test)]
254#[allow(clippy::expect_used)] mod tests {
256 use super::{shard_count, L1Cache, L1Entry, L1SwrRead};
257 use bytes::Bytes;
258 use std::time::{Duration, Instant};
259
260 const LONG: Duration = Duration::from_secs(60);
261
262 #[test]
265 fn contains_is_false_after_ttl_expiry() {
266 let cache = L1Cache::new(16);
267 cache.set("k", b"v", Duration::from_millis(50));
268 assert!(cache.contains("k"));
269 std::thread::sleep(Duration::from_millis(100));
270 assert!(!cache.contains("k"));
271 }
272
273 #[test]
274 fn shard_count_follows_capacity_alone() {
275 for (capacity, shards) in [
276 (0, 1),
277 (1, 1),
278 (255, 1),
279 (256, 2),
280 (1000, 4),
281 (1023, 4),
282 (1024, 8),
283 (8191, 32),
284 (8192, 64),
285 (usize::MAX, 64),
286 ] {
287 assert_eq!(shard_count(capacity), shards, "capacity {capacity}");
288 }
289 assert_eq!(L1Cache::new(1000).store.num_shards(), 4);
290 }
291
292 #[test]
295 fn never_holds_more_than_its_capacity() {
296 for capacity in [0usize, 1, 2, 16, 127, 128, 255, 256, 1000, 1001, 4100] {
297 let cache = L1Cache::new(capacity);
298 assert!(
299 cache.store.capacity() <= capacity as u64,
300 "capacity {capacity}"
301 );
302 for i in 0..capacity * 3 + 10 {
303 cache.set(&format!("key-{i}"), b"v", LONG);
304 assert!(
305 cache.store.len() <= capacity,
306 "capacity {capacity} after {i}"
307 );
308 }
309 }
310 }
311
312 #[test]
315 fn a_large_capacity_reserves_nothing_up_front() {
316 let cache = L1Cache::new(5_000_000);
317 assert_eq!(cache.store.memory_used().total(), 0);
318 cache.set("k", b"v", LONG);
319 assert!(cache.store.memory_used().total() > 0);
320 }
321
322 #[test]
323 fn zero_capacity_holds_nothing() {
324 let cache = L1Cache::new(0);
325 cache.set("k", b"v", LONG);
326 assert_eq!(cache.get("k"), None);
327 assert_eq!(cache.entry_count(), 0);
328 }
329
330 #[test]
333 fn a_read_that_finds_an_expired_entry_frees_it() {
334 let cache = L1Cache::new(16);
335 for key in ["get", "swr", "exists"] {
336 cache.set(key, b"v", Duration::from_millis(1));
337 }
338 std::thread::sleep(Duration::from_millis(20));
339 assert_eq!(cache.store.len(), 3);
340 assert_eq!(cache.get("get"), None);
341 assert_eq!(cache.get_with_swr("swr", 0.5), L1SwrRead::Miss);
342 assert!(!cache.contains("exists"));
343 assert_eq!(cache.store.len(), 0);
344 }
345
346 #[test]
349 fn expired_removal_spares_a_newer_write() {
350 let cache = L1Cache::new(16);
351 cache.set("k", b"old", Duration::from_millis(1));
352 std::thread::sleep(Duration::from_millis(20));
353 let read_at = Instant::now();
354 cache.set("k", b"new", LONG);
355 cache.remove_expired("k", read_at);
356 assert_eq!(cache.get("k").as_deref(), Some(&b"new"[..]));
357 }
358
359 #[test]
364 fn expired_removal_spares_a_replacement_with_the_same_stamp_and_buffer() {
365 let cache = L1Cache::new(16);
366 let stamp = Instant::now();
367 let written = |ttl| L1Entry {
368 data: Bytes::new(),
369 ttl,
370 created_at: stamp,
371 expires_at: stamp.checked_add(ttl),
372 freshness_jitter: 1.0,
373 };
374 cache
375 .store
376 .insert("k".to_owned(), written(Duration::from_nanos(1)));
377 let read_at = stamp + Duration::from_millis(1);
378 cache.store.insert("k".to_owned(), written(LONG));
379 cache.remove_expired("k", read_at);
380 assert!(
381 cache.store.peek("k").is_some(),
382 "the live replacement was removed"
383 );
384 }
385
386 #[test]
387 fn entry_count_sweeps_expired_entries() {
388 let cache = L1Cache::new(64);
389 for i in 0..5 {
390 cache.set(&format!("short-{i}"), b"v", Duration::from_millis(1));
391 }
392 for i in 0..3 {
393 cache.set(&format!("long-{i}"), b"v", LONG);
394 }
395 std::thread::sleep(Duration::from_millis(20));
396 assert_eq!(cache.store.len(), 8, "nothing has read or swept yet");
397 assert_eq!(cache.entry_count(), 3);
398 assert_eq!(cache.store.len(), 3);
399 }
400
401 #[test]
404 fn swr_goes_fresh_then_stale_then_miss() {
405 let ttl = Duration::from_secs(10);
406 let written = |ago: Duration| {
407 let created_at = Instant::now()
408 .checked_sub(ago)
409 .expect("the monotonic clock has run for more than 11 s");
410 L1Entry {
411 data: Bytes::from_static(b"v"),
412 ttl,
413 created_at,
414 expires_at: created_at.checked_add(ttl),
415 freshness_jitter: 1.0,
416 }
417 };
418 let cache = L1Cache::new(16);
419 for (key, ago) in [("fresh", 1), ("stale", 7), ("expired", 11)] {
420 cache
421 .store
422 .insert(key.to_owned(), written(Duration::from_secs(ago)));
423 }
424 let v = b"v".to_vec();
425 assert_eq!(
426 cache.get_with_swr("fresh", 0.5),
427 L1SwrRead::Fresh(v.clone())
428 );
429 assert_eq!(cache.get_with_swr("stale", 0.5), L1SwrRead::Stale(v));
430 assert_eq!(cache.get_with_swr("expired", 0.5), L1SwrRead::Miss);
431 }
432
433 #[test]
437 fn exists_does_not_count_as_a_read() {
438 let peeked = L1Cache::new(2);
439 peeked.set("a", b"1", LONG);
440 peeked.set("b", b"2", LONG);
441 assert!(peeked.contains("b"));
442 peeked.set("c", b"3", LONG);
443 assert!(peeked.store.peek("b").is_none(), "exists kept b alive");
444
445 let read = L1Cache::new(2);
446 read.set("a", b"1", LONG);
447 read.set("b", b"2", LONG);
448 assert!(read.get("b").is_some());
449 read.set("c", b"3", LONG);
450 assert!(
451 read.store.peek("b").is_some(),
452 "a read did not keep b alive"
453 );
454 }
455
456 #[test]
462 fn a_scan_does_not_flush_entries_that_were_read() {
463 fn kept_after_scan(read: bool) -> usize {
464 let cache = L1Cache::new(100);
465 for i in 0..100 {
466 cache.set(&format!("filler-{i}"), b"v", LONG);
467 }
468 for i in 0..50 {
469 let key = format!("hot-{i}");
470 cache.set(&key, b"v", LONG);
471 if read {
472 assert!(cache.get(&key).is_some());
473 assert!(cache.get(&key).is_some());
474 }
475 }
476 for i in 0..1000 {
477 cache.set(&format!("scan-{i}"), b"v", LONG);
478 }
479 (0..50)
480 .filter(|i| cache.store.peek(&format!("hot-{i}")).is_some())
481 .count()
482 }
483 assert_eq!(kept_after_scan(true), 50);
484 assert_eq!(kept_after_scan(false), 0);
485 }
486
487 #[test]
492 fn concurrent_mixed_operations_keep_the_invariants() {
493 const THREADS: u64 = 8;
494 const OPS: u64 = 20_000;
495 const KEYS: u64 = 64;
496 const CAPACITY: usize = 32;
497
498 fn next(state: &mut u64) -> u64 {
499 *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
500 let mut z = *state;
501 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
502 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
503 z ^ (z >> 31)
504 }
505
506 let cache = L1Cache::new(CAPACITY);
507 let workers: Vec<_> = (0..THREADS)
508 .map(|t| {
509 let cache = cache.clone();
510 std::thread::spawn(move || {
511 let mut rng = t;
512 for i in 0..OPS {
513 let key = format!("k{}", next(&mut rng) % KEYS);
514 let owned_by_key = |value: &[u8]| {
515 assert!(
516 value.starts_with(format!("{key}:").as_bytes()),
517 "{key} returned {}",
518 String::from_utf8_lossy(value)
519 );
520 };
521 match next(&mut rng) % 6 {
522 0 | 1 => {
523 let ttl = Duration::from_millis(1 + next(&mut rng) % 20);
524 cache.set(&key, format!("{key}:{t}:{i}").as_bytes(), ttl);
525 }
526 2 => {
527 if let Some(value) = cache.get(&key) {
528 owned_by_key(&value);
529 }
530 }
531 3 => match cache.get_with_swr(&key, 0.5) {
532 L1SwrRead::Fresh(value) | L1SwrRead::Stale(value) => {
533 owned_by_key(&value);
534 }
535 L1SwrRead::Miss => {}
536 },
537 4 => {
538 cache.contains(&key);
539 }
540 _ => cache.delete(&key),
541 }
542 assert!(cache.store.len() <= CAPACITY);
543 }
544 })
545 })
546 .collect();
547 for worker in workers {
548 worker.join().expect("a worker thread panicked");
549 }
550 assert!(cache.entry_count() <= CAPACITY as u64);
551 std::thread::sleep(Duration::from_millis(30));
552 assert_eq!(cache.entry_count(), 0);
553 }
554}