1type CacheResult<T> = Result<T, CacheError>;
4
5use crate::cache::ttl_millis;
6use crate::{Admission, AdmitMarker};
7use crate::{Cache, CacheError};
8use async_trait::async_trait;
9use lru::LruCache;
10use std::num::NonZeroUsize;
11use std::ops::{Deref, DerefMut};
12use std::sync::{Arc, Mutex, PoisonError};
13use std::time::{Duration, Instant};
14
15#[derive(Debug, Clone, Copy)]
21pub struct MemoryCacheLimits {
22 pub max_total_bytes: usize,
24 pub max_value_bytes: usize,
26 pub max_key_bytes: usize,
28}
29
30impl Default for MemoryCacheLimits {
31 fn default() -> Self {
32 Self {
33 max_total_bytes: 64 * 1024 * 1024,
34 max_value_bytes: 2 * 1024 * 1024,
35 max_key_bytes: 4096,
36 }
37 }
38}
39
40struct Store {
41 entries: LruCache<String, Entry>,
42 bytes: usize,
43 limits: MemoryCacheLimits,
44 started: Instant,
46}
47
48impl Deref for Store {
49 type Target = LruCache<String, Entry>;
50 fn deref(&self) -> &Self::Target {
51 &self.entries
52 }
53}
54
55impl DerefMut for Store {
56 fn deref_mut(&mut self) -> &mut Self::Target {
57 &mut self.entries
58 }
59}
60
61impl Store {
62 fn live(&mut self, key: &str, now: Instant) -> Option<&[u8]> {
64 if self.entries.peek(key).is_some_and(|e| e.is_expired(now)) {
65 self.pop(key);
66 return None;
67 }
68 self.entries.get(key).map(|entry| entry.value.as_slice())
69 }
70
71 fn pop(&mut self, key: &str) -> Option<Entry> {
72 let (key, entry) = self.entries.pop_entry(key)?;
73 self.bytes -= key.len() + entry.value.len();
74 Some(entry)
75 }
76
77 fn clear(&mut self) {
78 self.entries.clear();
79 self.bytes = 0;
80 }
81
82 fn put(&mut self, key: String, entry: Entry) -> Result<(), CacheError> {
83 let limits = self.limits;
84 let size = key
85 .len()
86 .checked_add(entry.value.len())
87 .ok_or(CacheError::SizeLimit)?;
88 if key.len() > limits.max_key_bytes
89 || entry.value.len() > limits.max_value_bytes
90 || size > limits.max_total_bytes
91 {
92 return Err(CacheError::SizeLimit);
93 }
94 self.pop(&key);
95 while self.bytes > limits.max_total_bytes - size
96 || self.entries.len() >= self.entries.cap().get()
97 {
98 let Some((key, entry)) = self.entries.pop_lru() else {
99 break;
100 };
101 self.bytes -= key.len() + entry.value.len();
102 }
103 self.bytes += size;
104 self.entries.put(key, entry);
105 Ok(())
106 }
107}
108
109struct Entry {
110 value: Vec<u8>,
111 expires_at: Option<Instant>,
112}
113
114impl Entry {
115 fn is_expired(&self, now: Instant) -> bool {
116 self.expires_at.is_some_and(|deadline| deadline <= now)
117 }
118}
119
120#[derive(Clone)]
136pub struct MemoryCache {
137 inner: Arc<Mutex<Store>>,
138}
139
140impl std::fmt::Debug for MemoryCache {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 let inner = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
143 f.debug_struct("MemoryCache")
144 .field("len", &inner.len())
145 .field("capacity", &inner.cap())
146 .field("stored_bytes", &inner.bytes)
147 .field("byte_limits", &inner.limits)
148 .finish()
149 }
150}
151
152impl MemoryCache {
153 #[must_use]
155 pub fn new(capacity: usize) -> Self {
156 Self::build(capacity, MemoryCacheLimits::default())
157 }
158
159 pub fn with_limits(n: usize, lim: MemoryCacheLimits) -> CacheResult<Self> {
171 if lim.max_total_bytes == 0
172 || lim.max_key_bytes == 0
173 || lim.max_value_bytes == 0
174 || lim.max_key_bytes > lim.max_total_bytes
175 || lim.max_value_bytes > lim.max_total_bytes
176 {
177 return Err(CacheError::InvalidLimits);
178 }
179 Ok(Self::build(n, lim))
180 }
181
182 fn build(capacity: usize, limits: MemoryCacheLimits) -> Self {
183 let size = NonZeroUsize::new(capacity.max(1));
184 let cap = size.unwrap_or(NonZeroUsize::MIN);
185 Self {
186 inner: Arc::new(Mutex::new(Store {
187 entries: LruCache::new(cap),
188 bytes: 0,
189 limits,
190 started: Instant::now(),
191 })),
192 }
193 }
194
195 #[must_use]
200 pub fn stored_bytes(&self) -> usize {
201 self.lock().bytes
202 }
203
204 fn lock(&self) -> std::sync::MutexGuard<'_, Store> {
205 self.inner.lock().unwrap_or_else(PoisonError::into_inner)
206 }
207}
208
209fn expires_at(ttl: Option<Duration>) -> Result<Option<Instant>, CacheError> {
210 match ttl {
211 Some(ttl) => Instant::now()
212 .checked_add(ttl)
213 .map(Some)
214 .ok_or(CacheError::InvalidTtl),
215 None => Ok(None),
216 }
217}
218
219fn parse_incr_int(bytes: &[u8]) -> Option<i64> {
221 let text = std::str::from_utf8(bytes).ok()?;
222 if text.is_empty() {
223 return None;
224 }
225 let digits = match text.as_bytes()[0] {
226 b'-' => &text.as_bytes()[1..],
227 b'0'..=b'9' => text.as_bytes(),
228 _ => return None,
229 };
230 if digits.is_empty() || !digits.iter().all(|b| b.is_ascii_digit()) {
231 return None;
232 }
233 text.parse().ok()
234}
235
236impl MemoryCache {
237 fn incr_locked(&self, key: &str, by: i64, expiry: Option<Duration>) -> CacheResult<i64> {
240 let mut cache = self.lock();
241 let now = Instant::now();
242 if cache.peek(key).is_some_and(|entry| entry.is_expired(now)) {
243 cache.pop(key);
244 }
245 let invalid = || CacheError::NotInteger {
246 key: key.to_owned(),
247 };
248 let (next, deadline) = match cache.get(key) {
249 Some(entry) => {
250 let current = parse_incr_int(&entry.value).ok_or_else(invalid)?;
251 let next = current.checked_add(by).ok_or_else(invalid)?;
252 (next, entry.expires_at)
253 }
254 None => (by, None),
255 };
256 let deadline = match (deadline, expiry) {
257 (None, Some(ttl)) => Some(now.checked_add(ttl).ok_or(CacheError::InvalidTtl)?),
258 (existing, _) => existing,
259 };
260 cache.put(
261 key.to_owned(),
262 Entry {
263 value: next.to_string().into_bytes(),
264 expires_at: deadline,
265 },
266 )?;
267 Ok(next)
268 }
269}
270
271#[async_trait]
272impl Cache for MemoryCache {
273 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, CacheError> {
274 let mut cache = self.lock();
275 let now = Instant::now();
276 let expired = cache.peek(key).is_some_and(|e| e.is_expired(now));
277 if expired {
278 cache.pop(key);
279 return Ok(None);
280 }
281 Ok(cache.get(key).map(|entry| entry.value.clone()))
282 }
283
284 async fn set(
285 &self,
286 key: &str,
287 value: Vec<u8>,
288 ttl: Option<Duration>,
289 ) -> Result<(), CacheError> {
290 let mut cache = self.lock();
291 cache.put(
292 key.to_owned(),
293 Entry {
294 value,
295 expires_at: expires_at(ttl)?,
296 },
297 )
298 }
299
300 async fn set_if_absent(&self, key: &str, v: Vec<u8>) -> CacheResult<bool> {
301 let mut cache = self.lock();
302 let now = Instant::now();
303 if cache.peek(key).is_some_and(|entry| !entry.is_expired(now)) {
304 return Ok(false);
305 }
306 cache.put(
307 key.to_owned(),
308 Entry {
309 value: v,
310 expires_at: None,
311 },
312 )?;
313 Ok(true)
314 }
315
316 async fn set_if_absent_with_ttl(
317 &self,
318 key: &str,
319 value: Vec<u8>,
320 ttl: Duration,
321 ) -> CacheResult<bool> {
322 let ttl = Duration::from_millis(ttl_millis(ttl)?);
323 let mut cache = self.lock();
326 let now = Instant::now();
327 if cache.peek(key).is_some_and(|entry| !entry.is_expired(now)) {
328 return Ok(false);
329 }
330 let deadline = now.checked_add(ttl).ok_or(CacheError::InvalidTtl)?;
331 cache.put(
332 key.to_owned(),
333 Entry {
334 value,
335 expires_at: Some(deadline),
336 },
337 )?;
338 Ok(true)
339 }
340
341 async fn delete(&self, key: &str) -> Result<bool, CacheError> {
342 let mut cache = self.lock();
343 let now = Instant::now();
344 match cache.peek(key) {
345 Some(entry) if entry.is_expired(now) => {
346 cache.pop(key);
347 Ok(false)
348 }
349 Some(_) => {
350 cache.pop(key);
351 Ok(true)
352 }
353 None => Ok(false),
354 }
355 }
356
357 async fn increment(&self, key: &str, by: i64) -> Result<i64, CacheError> {
358 self.incr_locked(key, by, None)
359 }
360
361 async fn increment_with_expiry(
362 &self,
363 key: &str,
364 by: i64,
365 ttl: Duration,
366 ) -> Result<i64, CacheError> {
367 let ttl = Duration::from_millis(ttl_millis(ttl)?);
368 self.incr_locked(key, by, Some(ttl))
369 }
370
371 async fn clear(&self) -> Result<(), CacheError> {
372 self.lock().clear();
373 Ok(())
374 }
375
376 async fn admit_marker(&self, req: &AdmitMarker<'_>) -> CacheResult<Admission> {
377 let window = Duration::from_millis(ttl_millis(req.window)?);
378 let counter_ttl = window.checked_mul(2).ok_or(CacheError::InvalidTtl)?;
379 let mut cache = self.lock();
380 let now = Instant::now();
381 let epoch = cache.live(req.epoch_key, now);
382 if epoch.is_none_or(|current| current != req.epoch) {
383 return Ok(Admission::Stale);
384 }
385 if let Some(live) = cache.live(req.marker, now) {
386 return Ok(Admission::Marker(live.to_vec()));
387 }
388 let ms = window.as_millis().max(1);
390 let elapsed = now.saturating_duration_since(cache.started).as_millis();
391 let counter = format!("{}{}", req.budget_prefix, elapsed / ms);
392 let used = match cache.live(&counter, now) {
393 Some(value) => parse_incr_int(value).ok_or_else(|| CacheError::NotInteger {
394 key: counter.clone(),
395 })?,
396 None => 0,
397 };
398 let used = u64::try_from(used).unwrap_or(u64::MAX);
399 if used >= req.max_new {
400 let left = u64::try_from(ms - elapsed % ms).unwrap_or(u64::MAX);
401 let resets_in = Duration::from_millis(left);
402 return Ok(Admission::Exhausted { resets_in });
403 }
404 let counted = Entry {
405 value: (used + 1).to_string().into_bytes(),
406 expires_at: Some(now.checked_add(counter_ttl).ok_or(CacheError::InvalidTtl)?),
407 };
408 let counted = match cache.peek(&counter) {
410 Some(old) if !old.is_expired(now) => Entry {
411 expires_at: old.expires_at,
412 ..counted
413 },
414 _ => counted,
415 };
416 let deadline = now.checked_add(window).ok_or(CacheError::InvalidTtl)?;
417 let marker = Entry {
418 value: req.token.to_vec(),
419 expires_at: Some(deadline),
420 };
421 cache.put(req.marker.to_owned(), marker)?;
423 if let Err(error) = cache.put(counter, counted) {
424 cache.pop(req.marker);
425 return Err(error);
426 }
427 Ok(Admission::Marker(req.token.to_vec()))
428 }
429
430 async fn invalidate_marker(
431 &self,
432 marker: &str,
433 epoch_key: &str,
434 epoch: &[u8],
435 ttl: Duration,
436 ) -> CacheResult<()> {
437 let ttl = Duration::from_millis(ttl_millis(ttl)?);
438 let mut cache = self.lock();
439 let deadline = Instant::now().checked_add(ttl);
440 let deadline = deadline.ok_or(CacheError::InvalidTtl)?;
441 let entry = Entry {
442 value: epoch.to_vec(),
443 expires_at: Some(deadline),
444 };
445 cache.pop(marker);
447 cache.put(epoch_key.to_owned(), entry)?;
448 Ok(())
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use crate::CacheExt;
456 use std::thread;
457 use std::time::Duration;
458
459 #[tokio::test]
460 async fn get_set_delete_clear() {
461 let cache = MemoryCache::new(8);
462 assert_eq!(cache.get("k").await.unwrap(), None);
463 cache.set("k", b"v".to_vec(), None).await.unwrap();
464 assert_eq!(cache.get("k").await.unwrap().as_deref(), Some(&b"v"[..]));
465 assert!(cache.delete("k").await.unwrap());
466 assert!(!cache.delete("k").await.unwrap());
467 cache.set("a", b"1".to_vec(), None).await.unwrap();
468 cache.set("b", b"2".to_vec(), None).await.unwrap();
469 cache.clear().await.unwrap();
470 assert_eq!(cache.get("a").await.unwrap(), None);
471 assert_eq!(cache.get("b").await.unwrap(), None);
472 }
473
474 #[tokio::test]
475 async fn increment_with_expiry_sets_a_deadline_once() {
476 let cache = MemoryCache::new(8);
477 let ttl = Duration::from_millis(60);
478 assert_eq!(cache.increment_with_expiry("c", 1, ttl).await.unwrap(), 1);
479 thread::sleep(Duration::from_millis(40));
480 let long = Duration::from_secs(60);
482 assert_eq!(cache.increment_with_expiry("c", 2, long).await.unwrap(), 3);
483 thread::sleep(Duration::from_millis(40));
484 assert_eq!(cache.get("c").await.unwrap(), None);
485 assert_eq!(cache.increment_with_expiry("c", 1, long).await.unwrap(), 1);
487 cache.set("p", b"5".to_vec(), None).await.unwrap();
489 assert_eq!(cache.increment_with_expiry("p", 1, ttl).await.unwrap(), 6);
490 thread::sleep(Duration::from_millis(80));
491 assert_eq!(cache.get("p").await.unwrap(), None);
492 }
493
494 #[tokio::test]
495 async fn increment_with_expiry_validates_ttl_and_value() {
496 let cache = MemoryCache::new(8);
497 let zero = cache.increment_with_expiry("c", 1, Duration::ZERO).await;
498 assert!(matches!(zero, Err(CacheError::InvalidTtl)));
499 assert_eq!(cache.get("c").await.unwrap(), None);
500 cache.set("s", b"x".to_vec(), None).await.unwrap();
501 let bad = cache.increment_with_expiry("s", 1, Duration::from_secs(1));
502 assert!(matches!(bad.await, Err(CacheError::NotInteger { .. })));
503 }
504
505 fn request<'a>(marker: &'a str, token: &'a [u8], epoch: &'a [u8]) -> AdmitMarker<'a> {
506 let window = Duration::from_millis(80);
507 AdmitMarker::new(marker, token, "epoch", epoch).budget("budget:", 2, window)
508 }
509
510 #[tokio::test]
511 async fn admit_marker_checks_epoch_counts_creations_and_rolls_windows() {
512 let cache = MemoryCache::new(64);
513 let epoch: &[u8] = &[7];
514 cache.set("epoch", epoch.to_vec(), None).await.unwrap();
515 let stale = cache.admit_marker(&request("a", &[1], &[8])).await;
516 assert_eq!(stale.unwrap(), Admission::Stale);
517 let gone = AdmitMarker::new("a", &[1], "elsewhere", epoch);
519 assert_eq!(cache.admit_marker(&gone).await.unwrap(), Admission::Stale);
520 assert_eq!(cache.get("a").await.unwrap(), None, "stale creates nothing");
521 let a = cache
522 .admit_marker(&request("a", &[1], epoch))
523 .await
524 .unwrap();
525 assert_eq!(a, Admission::Marker(vec![1]));
526 let again = cache
528 .admit_marker(&request("a", &[9], epoch))
529 .await
530 .unwrap();
531 assert_eq!(again, Admission::Marker(vec![1]));
532 let b = cache
533 .admit_marker(&request("b", &[2], epoch))
534 .await
535 .unwrap();
536 assert_eq!(b, Admission::Marker(vec![2]));
537 let over = cache
538 .admit_marker(&request("c", &[3], epoch))
539 .await
540 .unwrap();
541 let Admission::Exhausted { resets_in } = over else {
542 panic!("expected exhaustion, got {over:?}");
543 };
544 assert!(resets_in <= Duration::from_millis(80));
545 assert_eq!(cache.get("c").await.unwrap(), None, "no marker over budget");
546 thread::sleep(Duration::from_millis(90));
548 let c = cache
549 .admit_marker(&request("c", &[3], epoch))
550 .await
551 .unwrap();
552 assert_eq!(c, Admission::Marker(vec![3]));
553 }
554
555 #[tokio::test]
556 async fn admit_marker_is_exact_under_concurrency() {
557 let cache = MemoryCache::new(1024);
558 cache.set("epoch", vec![7], None).await.unwrap();
559 let window = Duration::from_secs(3600);
560 let mut tasks = Vec::new();
561 for n in 0..100u8 {
562 let cache = cache.clone();
563 tasks.push(tokio::spawn(async move {
564 let marker = format!("m{n}");
565 let token = [n];
566 let base = AdmitMarker::new(&marker, &token, "epoch", &[7]);
567 let request = base.budget("budget:", 10, window);
568 cache.admit_marker(&request).await.unwrap()
569 }));
570 }
571 let mut admitted = 0;
572 for task in tasks {
573 admitted += usize::from(matches!(task.await.unwrap(), Admission::Marker(_)));
574 }
575 assert_eq!(admitted, 10);
576 }
577
578 #[tokio::test]
579 async fn invalidate_marker_replaces_epoch_and_deletes_marker() {
580 let cache = MemoryCache::new(8);
581 cache.set("marker", vec![1], None).await.unwrap();
582 let ttl = Duration::from_millis(40);
583 cache
584 .invalidate_marker("marker", "epoch", &[5], ttl)
585 .await
586 .unwrap();
587 assert_eq!(cache.get("marker").await.unwrap(), None);
588 assert_eq!(cache.get("epoch").await.unwrap(), Some(vec![5]));
589 thread::sleep(Duration::from_millis(60));
590 assert_eq!(cache.get("epoch").await.unwrap(), None, "epoch expires");
591 let zero = cache
592 .invalidate_marker("m", "e", &[1], Duration::ZERO)
593 .await;
594 assert!(matches!(zero, Err(CacheError::InvalidTtl)));
595 }
596
597 #[tokio::test]
598 async fn stored_bytes_tracks_keys_and_values() {
599 let cache = MemoryCache::new(8);
600 assert_eq!(cache.stored_bytes(), 0);
601 cache.set("key", b"value".to_vec(), None).await.unwrap();
602 assert_eq!(cache.stored_bytes(), 8);
603 cache.delete("key").await.unwrap();
604 assert_eq!(cache.stored_bytes(), 0);
605 }
606
607 #[tokio::test]
608 async fn clones_share_storage() {
609 let cache = MemoryCache::new(4);
610 let clone = cache.clone();
611 clone.set("k", b"v".to_vec(), None).await.unwrap();
612 assert_eq!(cache.get("k").await.unwrap().as_deref(), Some(&b"v"[..]));
613 }
614
615 #[tokio::test]
616 async fn expired_entries_are_missing() {
617 let cache = MemoryCache::new(4);
618 cache
619 .set("k", b"v".to_vec(), Some(Duration::from_millis(15)))
620 .await
621 .unwrap();
622 assert_eq!(cache.get("k").await.unwrap().as_deref(), Some(&b"v"[..]));
623 tokio::time::sleep(Duration::from_millis(40)).await;
624 assert_eq!(cache.get("k").await.unwrap(), None);
625 assert!(!cache.delete("k").await.unwrap());
626 }
627
628 #[tokio::test]
629 async fn lru_evicts_the_least_recently_used() {
630 let cache = MemoryCache::new(2);
631 cache.set("a", b"1".to_vec(), None).await.unwrap();
632 cache.set("b", b"2".to_vec(), None).await.unwrap();
633 assert_eq!(cache.get("a").await.unwrap().as_deref(), Some(&b"1"[..]));
634 cache.set("c", b"3".to_vec(), None).await.unwrap();
635 assert_eq!(cache.get("b").await.unwrap(), None);
636 assert_eq!(cache.get("a").await.unwrap().as_deref(), Some(&b"1"[..]));
637 assert_eq!(cache.get("c").await.unwrap().as_deref(), Some(&b"3"[..]));
638 }
639
640 #[tokio::test]
641 async fn zero_capacity_is_one() {
642 let cache = MemoryCache::new(0);
643 cache.set("a", b"1".to_vec(), None).await.unwrap();
644 cache.set("b", b"2".to_vec(), None).await.unwrap();
645 assert_eq!(cache.get("a").await.unwrap(), None);
646 assert_eq!(cache.get("b").await.unwrap().as_deref(), Some(&b"2"[..]));
647 }
648
649 #[tokio::test]
650 async fn increment_matches_redis_incrby() {
651 let cache = MemoryCache::new(8);
652 assert_eq!(cache.increment("n", 3).await.unwrap(), 3);
653 assert_eq!(cache.increment("n", 2).await.unwrap(), 5);
654 assert_eq!(cache.increment("n", -8).await.unwrap(), -3);
655 assert_eq!(cache.get("n").await.unwrap().as_deref(), Some(&b"-3"[..]));
656 cache.set("n", b"9".to_vec(), None).await.unwrap();
657 assert_eq!(cache.increment("n", 1).await.unwrap(), 10);
658 }
659
660 #[tokio::test]
661 async fn increment_rejects_non_integers_and_overflow() {
662 let cache = MemoryCache::new(8);
663 cache.set("s", b"hello".to_vec(), None).await.unwrap();
664 assert!(matches!(
665 cache.increment("s", 1).await.unwrap_err(),
666 CacheError::NotInteger { ref key } if key == "s"
667 ));
668 cache.set("plus", b"+1".to_vec(), None).await.unwrap();
669 assert!(matches!(
670 cache.increment("plus", 1).await.unwrap_err(),
671 CacheError::NotInteger { .. }
672 ));
673 cache.set("ws", b" 1".to_vec(), None).await.unwrap();
674 assert!(matches!(
675 cache.increment("ws", 1).await.unwrap_err(),
676 CacheError::NotInteger { .. }
677 ));
678 cache
679 .set("max", i64::MAX.to_string().into_bytes(), None)
680 .await
681 .unwrap();
682 assert!(matches!(
683 cache.increment("max", 1).await.unwrap_err(),
684 CacheError::NotInteger { ref key } if key == "max"
685 ));
686 }
687
688 #[tokio::test]
689 async fn increment_on_expired_key_starts_at_zero() {
690 let cache = MemoryCache::new(4);
691 cache
692 .set("n", b"9".to_vec(), Some(Duration::from_millis(15)))
693 .await
694 .unwrap();
695 tokio::time::sleep(Duration::from_millis(40)).await;
696 assert_eq!(cache.increment("n", 4).await.unwrap(), 4);
697 }
698
699 #[tokio::test]
700 async fn increment_preserves_ttl_of_a_live_key() {
701 let cache = MemoryCache::new(4);
702 cache
703 .set("n", b"1".to_vec(), Some(Duration::from_millis(80)))
704 .await
705 .unwrap();
706 assert_eq!(cache.increment("n", 1).await.unwrap(), 2);
707 tokio::time::sleep(Duration::from_millis(120)).await;
708 assert_eq!(cache.get("n").await.unwrap(), None);
709 }
710
711 #[tokio::test]
712 async fn concurrent_increments_are_serialized() {
713 let cache = MemoryCache::new(4);
714 let mut joins = Vec::new();
715 for _ in 0..32 {
716 let cache = cache.clone();
717 let worker = async move { cache.increment("n", 1).await };
718 joins.push(tokio::spawn(worker));
719 }
720 let mut total = 0;
721 for join in joins {
722 total += join.await.unwrap().unwrap();
723 }
724 assert_eq!(cache.get("n").await.unwrap().as_deref(), Some(&b"32"[..]));
725 assert_eq!(total, (1..=32).sum::<i64>());
726 }
727
728 #[tokio::test]
729 async fn poisoned_mutex_is_recovered() {
730 let cache = MemoryCache::new(4);
731 cache.set("k", b"v".to_vec(), None).await.unwrap();
732 let poison = cache.clone();
733 let _ = thread::spawn(move || {
734 let _guard = poison.lock();
735 panic!("poison");
736 })
737 .join();
738 assert_eq!(cache.get("k").await.unwrap().as_deref(), Some(&b"v"[..]));
739 cache.set("k", b"w".to_vec(), None).await.unwrap();
740 assert_eq!(cache.get("k").await.unwrap().as_deref(), Some(&b"w"[..]));
741 }
742
743 #[tokio::test]
744 async fn binary_values_roundtrip() {
745 let cache = MemoryCache::new(2);
746 let value = vec![0, 127, 128, 255];
747 cache.set("bin", value.clone(), None).await.unwrap();
748 assert_eq!(cache.get("bin").await.unwrap(), Some(value));
749 }
750
751 #[tokio::test]
752 async fn set_replaces_ttl() {
753 let cache = MemoryCache::new(2);
754 cache
755 .set("k", b"1".to_vec(), Some(Duration::from_millis(15)))
756 .await
757 .unwrap();
758 cache.set("k", b"2".to_vec(), None).await.unwrap();
759 tokio::time::sleep(Duration::from_millis(40)).await;
760 assert_eq!(cache.get("k").await.unwrap().as_deref(), Some(&b"2"[..]));
761 }
762
763 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
764 async fn expiring_creation_has_one_winner() {
765 let cache = MemoryCache::new(8);
766 let barrier = std::sync::Arc::new(tokio::sync::Barrier::new(64));
767 let mut joins = Vec::new();
768 for v in 0..64u8 {
769 let cache = cache.clone();
770 let barrier = std::sync::Arc::clone(&barrier);
771 joins.push(tokio::spawn(async move {
772 let ttl = Duration::from_secs(30);
773 barrier.wait().await;
774 cache.set_if_absent_with_ttl("k", vec![v], ttl).await
775 }));
776 }
777 let mut winners = Vec::new();
778 for (v, join) in joins.into_iter().enumerate() {
779 if join.await.unwrap().unwrap() {
780 winners.push(v as u8);
781 }
782 }
783 assert_eq!(winners.len(), 1);
784 assert_eq!(cache.get("k").await.unwrap(), Some(winners));
785 }
786
787 #[tokio::test]
788 async fn expiring_creation_rejects_zero_before_touching_state() {
789 let cache = MemoryCache::new(4);
790 cache.set("k", b"live".to_vec(), None).await.unwrap();
791 let err = cache.set_if_absent_with_ttl("k", vec![1], Duration::ZERO);
792 assert!(matches!(err.await, Err(CacheError::InvalidTtl)));
793 let err = cache.set_if_absent_with_ttl("new", vec![1], Duration::ZERO);
794 assert!(matches!(err.await, Err(CacheError::InvalidTtl)));
795 assert_eq!(cache.get("k").await.unwrap().as_deref(), Some(&b"live"[..]));
796 assert_eq!(cache.get("new").await.unwrap(), None);
797 let huge = cache.set_if_absent_with_ttl("new", vec![1], Duration::MAX);
798 assert!(matches!(huge.await, Err(CacheError::InvalidTtl)));
799 }
800
801 #[tokio::test]
802 async fn expiring_creation_rounds_subunit_ttl_up() {
803 let cache = MemoryCache::new(4);
804 let before = Instant::now();
805 let tiny = Duration::from_nanos(1);
806 assert!(
807 cache
808 .set_if_absent_with_ttl("k", vec![1], tiny)
809 .await
810 .unwrap()
811 );
812 let deadline = cache.lock().peek("k").and_then(|e| e.expires_at);
813 let deadline = deadline.expect("entry has a deadline");
814 assert!(deadline >= before + Duration::from_millis(1));
815 let odd = Duration::from_micros(1500);
816 assert!(
817 cache
818 .set_if_absent_with_ttl("j", vec![1], odd)
819 .await
820 .unwrap()
821 );
822 let before = Instant::now();
823 let deadline = cache.lock().peek("j").and_then(|e| e.expires_at);
824 assert!(deadline.expect("deadline") <= before + Duration::from_millis(2));
825 }
826
827 #[tokio::test]
828 async fn expiring_creation_replaces_expired_and_expires() {
829 let cache = MemoryCache::new(4);
830 let ms = Duration::from_millis(5);
831 assert!(
832 cache
833 .set_if_absent_with_ttl("k", vec![1], ms)
834 .await
835 .unwrap()
836 );
837 assert!(
838 !cache
839 .set_if_absent_with_ttl("k", vec![2], ms)
840 .await
841 .unwrap()
842 );
843 tokio::time::sleep(Duration::from_millis(30)).await;
844 let long = Duration::from_secs(30);
846 assert!(
847 cache
848 .set_if_absent_with_ttl("k", vec![3], long)
849 .await
850 .unwrap()
851 );
852 assert_eq!(cache.get("k").await.unwrap(), Some(vec![3]));
853 let one = Duration::from_millis(5);
855 assert!(
856 !cache
857 .set_if_absent_with_ttl("k", vec![4], one)
858 .await
859 .unwrap()
860 );
861 tokio::time::sleep(Duration::from_millis(30)).await;
862 assert_eq!(cache.get("k").await.unwrap(), Some(vec![3]));
863 }
864
865 #[tokio::test]
866 async fn expiring_creation_obeys_byte_admission() {
867 let lim = MemoryCacheLimits {
868 max_total_bytes: 8,
869 max_value_bytes: 4,
870 max_key_bytes: 4,
871 };
872 let cache = MemoryCache::with_limits(4, lim).unwrap();
873 let ttl = Duration::from_secs(1);
874 let err = cache.set_if_absent_with_ttl("k", vec![0; 5], ttl).await;
875 assert!(matches!(err, Err(CacheError::SizeLimit)));
876 assert_eq!(cache.get("k").await.unwrap(), None);
877 }
878
879 #[test]
880 fn parse_incr_int_is_strict() {
881 assert_eq!(parse_incr_int(b"0"), Some(0));
882 assert_eq!(parse_incr_int(b"01"), Some(1));
883 assert_eq!(parse_incr_int(b"-12"), Some(-12));
884 assert_eq!(parse_incr_int(b""), None);
885 assert_eq!(parse_incr_int(b"-"), None);
886 assert_eq!(parse_incr_int(b"+1"), None);
887 assert_eq!(parse_incr_int(b"1 "), None);
888 assert_eq!(parse_incr_int(b"1.0"), None);
889 assert_eq!(parse_incr_int(b"abc"), None);
890 }
891
892 #[test]
893 fn types_are_send_sync() {
894 fn assert_send_sync<T: Send + Sync>() {}
895 assert_send_sync::<MemoryCache>();
896 assert_send_sync::<Box<dyn Cache>>();
897 }
898
899 #[tokio::test]
900 async fn json_helper_uses_compact_encoding() {
901 let cache = MemoryCache::new(2);
902 cache.set_json("n", &42u32, None).await.unwrap();
903 assert_eq!(cache.get("n").await.unwrap().as_deref(), Some(&b"42"[..]));
904 }
905}
906
907#[cfg(test)]
908#[path = "memory_limits_tests.rs"]
909mod limits_tests;