1use async_lock::{Mutex as AsyncMutex, RwLock};
12use std::borrow::Borrow;
13use std::collections::{BTreeMap, HashMap};
14use std::hash::{BuildHasher, Hash, RandomState};
15use std::sync::Arc;
16use std::time::Duration;
17use wacore::runtime::BoxFuture;
18use wacore::sync_marker::MaybeSend;
19use wacore::time::Instant;
20
21struct CacheEntry<V> {
22 value: V,
23 inserted_at: Instant,
26 last_accessed_at: Instant,
27 seq: u64,
29}
30
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
32pub(crate) struct CapacityStats {
33 pub entries: u64,
34 pub evictions: u64,
35 pub eviction_blocks: u64,
36}
37
38pub struct PortableCache<K, V> {
44 inner: Arc<RwLock<CacheInner<K, V>>>,
45 init_locks: Arc<InitLocks>,
47 max_capacity: Option<u64>,
48 ttl: Option<Duration>,
49 tti: Option<Duration>,
50 evict_guard: Option<fn(&V) -> bool>,
55}
56
57struct CacheInner<K, V> {
58 map: HashMap<K, CacheEntry<V>>,
59 order: BTreeMap<u64, K>,
63 next_seq: u64,
65 capacity_evictions: u64,
66 capacity_eviction_blocks: u64,
67}
68
69impl<K, V> CacheInner<K, V>
70where
71 K: Hash + Eq + Clone,
72{
73 fn new() -> Self {
74 Self {
75 map: HashMap::new(),
76 order: BTreeMap::new(),
77 next_seq: 0,
78 capacity_evictions: 0,
79 capacity_eviction_blocks: 0,
80 }
81 }
82
83 fn remove_key(&mut self, key: &K) -> Option<CacheEntry<V>> {
84 let entry = self.map.remove(key)?;
85 self.order.remove(&entry.seq);
86 Some(entry)
87 }
88
89 #[inline(never)]
93 fn evict_to_capacity(&mut self, cap: u64, evict_guard: Option<fn(&V) -> bool>) {
94 while self.map.len() as u64 >= cap {
95 match evict_guard {
96 None => match self.order.pop_first() {
98 Some((_, oldest_key)) => {
99 if self.map.remove(&oldest_key).is_some() {
100 self.capacity_evictions = self.capacity_evictions.saturating_add(1);
101 }
102 }
103 None => break,
104 },
105 Some(is_evictable) => {
110 let mut victim_seq = None;
111 for (seq, k) in self.order.iter() {
112 if self.map.get(k).is_some_and(|e| is_evictable(&e.value)) {
113 victim_seq = Some(*seq);
114 break;
115 }
116 }
117 match victim_seq {
118 Some(seq) => {
119 if let Some(oldest_key) = self.order.remove(&seq)
120 && self.map.remove(&oldest_key).is_some()
121 {
122 self.capacity_evictions = self.capacity_evictions.saturating_add(1);
123 }
124 }
125 None => {
126 self.capacity_eviction_blocks =
127 self.capacity_eviction_blocks.saturating_add(1);
128 break;
129 }
130 }
131 }
132 }
133 }
134 }
135
136 fn insert_new(
140 &mut self,
141 key: K,
142 value: V,
143 now: Instant,
144 max_capacity: Option<u64>,
145 evict_guard: Option<fn(&V) -> bool>,
146 ) {
147 if let Some(cap) = max_capacity {
148 self.evict_to_capacity(cap, evict_guard);
149 }
150
151 let seq = self.next_seq;
152 self.next_seq += 1;
153 self.order.insert(seq, key.clone());
154 self.map.insert(
155 key,
156 CacheEntry {
157 value,
158 inserted_at: now,
159 last_accessed_at: now,
160 seq,
161 },
162 );
163 }
164}
165
166struct InitLocks {
173 hasher: RandomState,
175 map: AsyncMutex<HashMap<u64, Arc<AsyncMutex<()>>>>,
176}
177
178impl InitLocks {
179 fn new() -> Self {
180 Self {
181 hasher: RandomState::new(),
182 map: AsyncMutex::new(HashMap::new()),
183 }
184 }
185
186 fn hash_of<Q: Hash + ?Sized>(&self, key: &Q) -> u64 {
187 self.hasher.hash_one(key)
188 }
189
190 async fn acquire(&self, hash: u64) -> Arc<AsyncMutex<()>> {
191 let mut locks = self.map.lock().await;
192 locks
193 .entry(hash)
194 .or_insert_with(|| Arc::new(AsyncMutex::new(())))
195 .clone()
196 }
197
198 async fn reclaim(&self, hash: u64, init_mutex: &Arc<AsyncMutex<()>>) {
205 let mut locks = self.map.lock().await;
206 if Arc::strong_count(init_mutex) <= 2
207 && let Some(existing) = locks.get(&hash)
208 && Arc::ptr_eq(existing, init_mutex)
209 {
210 locks.remove(&hash);
211 }
212 }
213
214 fn reclaim_now(&self, hash: u64, init_mutex: &Arc<AsyncMutex<()>>) {
218 if let Some(mut locks) = self.map.try_lock()
219 && Arc::strong_count(init_mutex) <= 2
220 && let Some(existing) = locks.get(&hash)
221 && Arc::ptr_eq(existing, init_mutex)
222 {
223 locks.remove(&hash);
224 }
225 }
226
227 async fn retain_active(&self) {
228 let mut locks = self.map.lock().await;
229 locks.retain(|_, v| Arc::strong_count(v) > 1);
230 }
231}
232
233struct InitLockCleanup<'a> {
238 registry: &'a InitLocks,
239 hash: u64,
240 lock: Option<Arc<AsyncMutex<()>>>,
241}
242
243impl InitLockCleanup<'_> {
244 fn disarm(&mut self) -> Arc<AsyncMutex<()>> {
245 self.lock.take().expect("init-lock cleanup disarmed twice")
246 }
247}
248
249impl Drop for InitLockCleanup<'_> {
250 fn drop(&mut self) {
251 if let Some(lock) = self.lock.take() {
252 self.registry.reclaim_now(self.hash, &lock);
253 }
254 }
255}
256
257pub struct PortableCacheBuilder<K, V> {
260 max_capacity: Option<u64>,
261 ttl: Option<Duration>,
262 tti: Option<Duration>,
263 evict_guard: Option<fn(&V) -> bool>,
264 _marker: std::marker::PhantomData<fn(K, V)>,
265}
266
267impl<K, V> PortableCacheBuilder<K, V>
268where
269 K: Hash + Eq + Clone + Send + Sync + 'static,
270 V: Clone + Send + Sync + 'static,
271{
272 fn new() -> Self {
273 Self {
274 max_capacity: None,
275 ttl: None,
276 tti: None,
277 evict_guard: None,
278 _marker: std::marker::PhantomData,
279 }
280 }
281
282 pub fn evict_guard(mut self, guard: fn(&V) -> bool) -> Self {
287 self.evict_guard = Some(guard);
288 self
289 }
290
291 pub fn max_capacity(mut self, cap: u64) -> Self {
292 self.max_capacity = Some(cap);
293 self
294 }
295
296 pub fn time_to_live(mut self, ttl: Duration) -> Self {
297 self.ttl = Some(ttl);
298 self
299 }
300
301 pub fn time_to_idle(mut self, tti: Duration) -> Self {
302 self.tti = Some(tti);
303 self
304 }
305
306 pub fn build(self) -> PortableCache<K, V> {
307 PortableCache {
308 inner: Arc::new(RwLock::new(CacheInner::new())),
309 init_locks: Arc::new(InitLocks::new()),
310 max_capacity: self.max_capacity,
311 ttl: self.ttl,
312 tti: self.tti,
313 evict_guard: self.evict_guard,
314 }
315 }
316}
317
318impl<K, V> PortableCache<K, V>
321where
322 K: Hash + Eq + Clone + Send + Sync + 'static,
323 V: Clone + Send + Sync + 'static,
324{
325 pub fn builder() -> PortableCacheBuilder<K, V> {
326 PortableCacheBuilder::new()
327 }
328
329 #[inline]
333 fn entry_time(&self) -> Instant {
334 if self.ttl.is_some() || self.tti.is_some() {
335 Instant::now()
336 } else {
337 Instant::ZERO
338 }
339 }
340
341 fn is_expired(&self, entry: &CacheEntry<V>, now: Instant) -> bool {
342 if let Some(ttl) = self.ttl
343 && now.saturating_duration_since(entry.inserted_at) >= ttl
344 {
345 return true;
346 }
347 if let Some(tti) = self.tti
348 && now.saturating_duration_since(entry.last_accessed_at) >= tti
349 {
350 return true;
351 }
352 false
353 }
354
355 fn find_key<Q>(inner: &CacheInner<K, V>, key: &Q) -> Option<K>
356 where
357 K: Borrow<Q>,
358 Q: Hash + Eq + ?Sized,
359 {
360 inner.map.get_key_value(key).map(|(k, _)| k.clone())
361 }
362
363 pub async fn get<Q>(&self, key: &Q) -> Option<V>
364 where
365 K: Borrow<Q>,
366 Q: Hash + Eq + ?Sized,
367 {
368 if self.tti.is_none() {
370 let guard = self.inner.read().await;
371 let entry = guard.map.get(key)?;
372 let now = self.entry_time();
376 if self.is_expired(entry, now) {
377 let owned_key = Self::find_key(&guard, key)?;
378 drop(guard);
379 let mut wguard = self.inner.write().await;
380 if let Some(e) = wguard.map.get(key)
381 && self.is_expired(e, now)
382 {
383 wguard.remove_key(&owned_key);
384 }
385 return None;
386 }
387 return Some(entry.value.clone());
388 }
389
390 let mut guard = self.inner.write().await;
392 let entry = guard.map.get_mut(key)?;
393 let now = self.entry_time();
394 if self.is_expired(entry, now) {
395 let owned_key = Self::find_key(&guard, key)?;
396 guard.remove_key(&owned_key);
397 return None;
398 }
399 entry.last_accessed_at = now;
400 Some(entry.value.clone())
401 }
402
403 pub async fn insert(&self, key: K, value: V) {
404 let now = self.entry_time();
405 let mut guard = self.inner.write().await;
406
407 if let Some(entry) = guard.map.get_mut(&key) {
408 entry.value = value;
409 entry.inserted_at = now;
410 entry.last_accessed_at = now;
411 return;
412 }
413
414 if self.max_capacity == Some(0) {
415 return;
416 }
417
418 guard.insert_new(key, value, now, self.max_capacity, self.evict_guard);
419 }
420
421 pub async fn upsert_with_by_ref<Q, R>(
428 &self,
429 key: &Q,
430 update: impl FnOnce(Option<&V>) -> (Option<V>, R),
431 ) -> R
432 where
433 K: Borrow<Q>,
434 Q: ToOwned<Owned = K> + Hash + Eq + ?Sized,
435 {
436 let now = self.entry_time();
437 let mut guard = self.inner.write().await;
438
439 if guard
440 .map
441 .get(key)
442 .is_some_and(|entry| self.is_expired(entry, now))
443 && let Some(owned_key) = Self::find_key(&guard, key)
444 {
445 guard.remove_key(&owned_key);
446 }
447
448 let (next, result) = update(guard.map.get(key).map(|entry| &entry.value));
449 let Some(next) = next else {
450 return result;
451 };
452
453 if let Some(entry) = guard.map.get_mut(key) {
454 entry.value = next;
455 entry.inserted_at = now;
456 entry.last_accessed_at = now;
457 } else if self.max_capacity != Some(0) {
458 guard.insert_new(
459 key.to_owned(),
460 next,
461 now,
462 self.max_capacity,
463 self.evict_guard,
464 );
465 }
466
467 result
468 }
469
470 async fn insert_and_return(&self, key: K, value: V) -> V {
472 let now = self.entry_time();
473 let mut guard = self.inner.write().await;
474
475 if let Some(entry) = guard.map.get_mut(&key) {
476 let ret = value.clone();
477 entry.value = value;
478 entry.inserted_at = now;
479 entry.last_accessed_at = now;
480 return ret;
481 }
482
483 if self.max_capacity == Some(0) {
484 return value;
485 }
486
487 let ret = value.clone();
488 guard.insert_new(key, value, now, self.max_capacity, self.evict_guard);
489 ret
490 }
491
492 pub async fn remove<Q>(&self, key: &Q) -> Option<V>
493 where
494 K: Borrow<Q>,
495 Q: Hash + Eq + ?Sized,
496 {
497 let mut guard = self.inner.write().await;
498 let owned_key = Self::find_key(&guard, key)?;
499 let entry = guard.remove_key(&owned_key)?;
500 let now = self.entry_time();
502 if self.is_expired(&entry, now) {
503 None
504 } else {
505 Some(entry.value)
506 }
507 }
508
509 pub async fn invalidate<Q>(&self, key: &Q)
510 where
511 K: Borrow<Q>,
512 Q: Hash + Eq + ?Sized,
513 {
514 let mut guard = self.inner.write().await;
515 if let Some(owned_key) = Self::find_key(&guard, key) {
516 guard.remove_key(&owned_key);
517 }
518 }
519
520 pub async fn clear(&self) {
525 let mut guard = self.inner.write().await;
526 guard.map.clear();
527 guard.order.clear();
528 }
529
530 pub fn invalidate_all(&self) {
534 for _ in 0..64 {
535 if let Some(mut guard) = self.inner.try_write() {
536 guard.map.clear();
537 guard.order.clear();
538 return;
539 }
540 std::hint::spin_loop();
541 }
542 log::warn!("PortableCache::invalidate_all: could not acquire write lock after retries");
543 }
544
545 pub fn entry_count(&self) -> u64 {
546 self.inner
547 .try_read()
548 .map(|g| g.map.len() as u64)
549 .unwrap_or(0)
550 }
551
552 pub(crate) async fn capacity_stats(&self) -> CapacityStats {
553 let guard = self.inner.read().await;
554 CapacityStats {
555 entries: guard.map.len() as u64,
556 evictions: guard.capacity_evictions,
557 eviction_blocks: guard.capacity_eviction_blocks,
558 }
559 }
560
561 pub async fn snapshot_entries(&self) -> Vec<(Arc<K>, V)> {
566 let guard = self.inner.read().await;
567 Self::snapshot(&guard)
568 }
569
570 pub async fn fold_entries<A>(&self, init: A, mut f: impl FnMut(A, &K, &V) -> A) -> A {
575 let guard = self.inner.read().await;
576 guard
577 .map
578 .iter()
579 .fold(init, |acc, (k, e)| f(acc, k, &e.value))
580 }
581
582 pub async fn memory_stats(
586 &self,
587 mut per_entry: impl FnMut(&K, &V) -> usize,
588 ) -> wacore::stats::CollectionStats {
589 let guard = self.inner.read().await;
590 let bytes: usize = guard.map.iter().map(|(k, e)| per_entry(k, &e.value)).sum();
591 wacore::stats::CollectionStats::new(guard.map.len() as u64, bytes as u64)
592 }
593
594 pub fn iter(&self) -> std::vec::IntoIter<(Arc<K>, V)> {
601 for _ in 0..1024 {
602 if let Some(guard) = self.inner.try_read() {
603 return Self::snapshot(&guard).into_iter();
604 }
605 std::hint::spin_loop();
606 }
607 log::warn!(
608 "PortableCache::iter: could not acquire read lock after retries; \
609 returning empty snapshot"
610 );
611 Vec::new().into_iter()
612 }
613
614 fn snapshot(guard: &CacheInner<K, V>) -> Vec<(Arc<K>, V)> {
615 guard
616 .map
617 .iter()
618 .map(|(k, e)| (Arc::new(k.clone()), e.value.clone()))
619 .collect()
620 }
621
622 #[inline]
632 pub async fn get_with<F>(&self, key: K, init: F) -> V
633 where
634 F: Future<Output = V> + MaybeSend,
635 {
636 if let Some(v) = self.get(&key).await {
637 return v;
638 }
639 self.get_with_slow(key, Box::pin(init)).await
640 }
641
642 #[inline]
645 pub async fn get_with_by_ref<Q, F>(&self, key: &Q, init: F) -> V
646 where
647 K: Borrow<Q>,
648 Q: ToOwned<Owned = K> + Hash + Eq + ?Sized,
649 F: Future<Output = V> + MaybeSend,
650 {
651 if let Some(v) = self.get(key).await {
652 return v;
653 }
654 self.get_with_slow(key.to_owned(), Box::pin(init)).await
655 }
656
657 async fn get_with_slow(&self, key: K, init: BoxFuture<'_, V>) -> V {
662 let hash = self.init_locks.hash_of(&key);
663 let mut cleanup = InitLockCleanup {
666 registry: &self.init_locks,
667 hash,
668 lock: Some(self.init_locks.acquire(hash).await),
669 };
670
671 let value = {
672 let _init_guard = cleanup
673 .lock
674 .as_ref()
675 .expect("init-lock cleanup still armed")
676 .lock()
677 .await;
678 if let Some(v) = self.get(&key).await {
680 v
681 } else {
682 let value = init.await;
683 self.insert_and_return(key, value).await
684 }
685 };
686
687 let init_mutex = cleanup.disarm();
688 drop(cleanup);
689 self.init_locks.reclaim(hash, &init_mutex).await;
690 value
691 }
692
693 pub async fn run_pending_tasks(&self) {
695 let now = self.entry_time();
696 let mut guard = self.inner.write().await;
697
698 guard.map.retain(|_, entry| !self.is_expired(entry, now));
699
700 let CacheInner { map, order, .. } = &mut *guard;
703 order.retain(|_, k| map.contains_key(k));
704
705 drop(guard);
706
707 self.init_locks.retain_active().await;
709 }
710}
711
712impl<K, V> Clone for PortableCache<K, V> {
713 fn clone(&self) -> Self {
714 Self {
715 inner: Arc::clone(&self.inner),
716 init_locks: Arc::clone(&self.init_locks),
717 max_capacity: self.max_capacity,
718 ttl: self.ttl,
719 tti: self.tti,
720 evict_guard: self.evict_guard,
721 }
722 }
723}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728 use std::sync::atomic::{AtomicUsize, Ordering};
729
730 fn build_cache<K, V>() -> PortableCache<K, V>
731 where
732 K: Hash + Eq + Clone + Send + Sync + 'static,
733 V: Clone + Send + Sync + 'static,
734 {
735 PortableCache::builder().max_capacity(100).build()
736 }
737
738 #[tokio::test]
739 async fn test_basic_insert_and_get() {
740 let cache = build_cache::<String, String>();
741
742 assert!(cache.get("key1").await.is_none());
743
744 cache.insert("key1".to_string(), "value1".to_string()).await;
745 assert_eq!(cache.get("key1").await, Some("value1".to_string()));
746 }
747
748 #[tokio::test]
749 async fn capacity_only_cache_uses_clock_free_timestamps() {
750 let cache = build_cache::<String, String>();
751 assert_eq!(cache.entry_time(), Instant::ZERO);
752
753 cache.insert("key".into(), "value".into()).await;
754 assert_eq!(cache.get("key").await.as_deref(), Some("value"));
755
756 let guard = cache.inner.read().await;
757 let entry = guard.map.get("key").expect("inserted cache entry");
758 assert_eq!(entry.inserted_at, Instant::ZERO);
759 assert_eq!(entry.last_accessed_at, Instant::ZERO);
760 }
761
762 #[tokio::test]
763 async fn test_update_existing_key() {
764 let cache = build_cache::<String, String>();
765
766 cache.insert("key1".to_string(), "v1".to_string()).await;
767 cache.insert("key1".to_string(), "v2".to_string()).await;
768 assert_eq!(cache.get("key1").await, Some("v2".to_string()));
769 assert_eq!(cache.entry_count(), 1);
770 }
771
772 #[tokio::test]
773 async fn upsert_with_by_ref_serializes_read_modify_write() {
774 let cache = Arc::new(build_cache::<String, u32>());
775 let mut tasks = Vec::new();
776 for _ in 0..32 {
777 let cache = Arc::clone(&cache);
778 tasks.push(tokio::spawn(async move {
779 cache
780 .upsert_with_by_ref("counter", |current| {
781 let next = current.copied().unwrap_or_default() + 1;
782 (Some(next), next)
783 })
784 .await
785 }));
786 }
787
788 let mut results = Vec::with_capacity(tasks.len());
789 for task in tasks {
790 results.push(task.await.unwrap());
791 }
792 results.sort_unstable();
793
794 assert_eq!(results, (1..=32).collect::<Vec<_>>());
795 assert_eq!(cache.get("counter").await, Some(32));
796
797 let unchanged = cache
798 .upsert_with_by_ref("counter", |current| (None, current.copied()))
799 .await;
800 assert_eq!(unchanged, Some(32));
801 assert_eq!(cache.get("counter").await, Some(32));
802 }
803
804 #[tokio::test]
805 async fn test_capacity_eviction() {
806 let cache: PortableCache<String, u32> = PortableCache::builder().max_capacity(3).build();
807
808 cache.insert("a".into(), 1).await;
809 cache.insert("b".into(), 2).await;
810 cache.insert("c".into(), 3).await;
811 assert_eq!(cache.entry_count(), 3);
812
813 cache.insert("d".into(), 4).await;
814 assert_eq!(cache.entry_count(), 3);
815 assert!(cache.get("a").await.is_none());
816 assert_eq!(cache.get("b").await, Some(2));
817 assert_eq!(cache.get("d").await, Some(4));
818 assert_eq!(
819 cache.capacity_stats().await,
820 CapacityStats {
821 entries: 3,
822 evictions: 1,
823 eviction_blocks: 0,
824 }
825 );
826 }
827
828 #[tokio::test]
829 async fn evict_guard_protects_held_entries() {
830 type Lock = Arc<AsyncMutex<()>>;
831 let guarded: PortableCache<String, Lock> = PortableCache::builder()
832 .max_capacity(2)
833 .evict_guard(|m| Arc::strong_count(m) <= 1)
834 .build();
835
836 let held: Lock = Arc::new(AsyncMutex::new(()));
838 guarded.insert("held".into(), held.clone()).await;
839
840 for i in 0..5 {
842 guarded
843 .insert(format!("k{i}"), Arc::new(AsyncMutex::new(())))
844 .await;
845 }
846
847 let again = guarded
850 .get("held")
851 .await
852 .expect("held entry must not be FIFO-evicted");
853 assert!(Arc::ptr_eq(&held, &again), "same mutex instance preserved");
854
855 let unguarded: PortableCache<String, Lock> =
857 PortableCache::builder().max_capacity(2).build();
858 unguarded.insert("held".into(), held.clone()).await;
859 for i in 0..5 {
860 unguarded
861 .insert(format!("k{i}"), Arc::new(AsyncMutex::new(())))
862 .await;
863 }
864 assert!(
865 unguarded.get("held").await.is_none(),
866 "an unguarded cache FIFO-evicts the held entry (the bug this guards)"
867 );
868 }
869
870 #[tokio::test]
871 async fn evict_guard_allows_temporary_over_capacity_when_all_held() {
872 type Lock = Arc<AsyncMutex<()>>;
873 let cache: PortableCache<String, Lock> = PortableCache::builder()
874 .max_capacity(2)
875 .evict_guard(|m| Arc::strong_count(m) <= 1)
876 .build();
877
878 let mut held = Vec::new();
881 for i in 0..3 {
882 let lock: Lock = Arc::new(AsyncMutex::new(()));
883 held.push(lock.clone());
884 cache.insert(format!("k{i}"), lock).await;
885 }
886 assert_eq!(
887 cache.entry_count(),
888 3,
889 "all entries held -> cache exceeds capacity instead of evicting a live lock"
890 );
891 assert_eq!(
892 cache.capacity_stats().await,
893 CapacityStats {
894 entries: 3,
895 evictions: 0,
896 eviction_blocks: 1,
897 }
898 );
899
900 drop(held);
902 cache
903 .insert("fresh".into(), Arc::new(AsyncMutex::new(())))
904 .await;
905 assert_eq!(
906 cache.entry_count(),
907 2,
908 "once entries are released, eviction resumes down to capacity"
909 );
910 assert_eq!(
911 cache.capacity_stats().await,
912 CapacityStats {
913 entries: 2,
914 evictions: 2,
915 eviction_blocks: 1,
916 }
917 );
918 }
919
920 #[tokio::test]
921 async fn test_remove_then_eviction_preserves_fifo_order() {
922 let cache: PortableCache<String, u32> = PortableCache::builder().max_capacity(3).build();
925 cache.insert("a".into(), 1).await;
926 cache.insert("b".into(), 2).await;
927 cache.insert("c".into(), 3).await;
928
929 assert_eq!(cache.remove("a").await, Some(1));
931 cache.insert("d".into(), 4).await; assert_eq!(cache.entry_count(), 3);
933
934 cache.insert("e".into(), 5).await;
936 assert_eq!(cache.entry_count(), 3);
937 assert!(cache.get("b").await.is_none(), "b was the oldest survivor");
938 assert_eq!(cache.get("c").await, Some(3));
939 assert_eq!(cache.get("d").await, Some(4));
940 assert_eq!(cache.get("e").await, Some(5));
941 }
942
943 #[tokio::test]
944 async fn test_zero_capacity_disables_caching() {
945 let cache: PortableCache<String, u32> = PortableCache::builder().max_capacity(0).build();
946
947 cache.insert("a".into(), 1).await;
948 assert!(cache.get("a").await.is_none());
949 assert_eq!(cache.entry_count(), 0);
950 }
951
952 #[test]
956 fn expiry_boundary_is_exact_under_a_controlled_clock() {
957 let ttl = Duration::from_secs(60);
958 let tti = Duration::from_secs(10);
959 let cache: PortableCache<String, u32> = PortableCache::builder()
960 .max_capacity(100)
961 .time_to_live(ttl)
962 .time_to_idle(tti)
963 .build();
964
965 let inserted = Instant::ZERO + Duration::from_secs(1_000);
966 let entry = CacheEntry {
967 value: 1,
968 inserted_at: inserted,
969 last_accessed_at: inserted,
970 seq: 0,
971 };
972
973 assert!(!cache.is_expired(&entry, inserted + tti - Duration::from_nanos(1)));
974 assert!(cache.is_expired(&entry, inserted + tti), "TTI is inclusive");
975
976 let idle_free: PortableCache<String, u32> = PortableCache::builder()
977 .max_capacity(100)
978 .time_to_live(ttl)
979 .build();
980 assert!(!idle_free.is_expired(&entry, inserted + ttl - Duration::from_nanos(1)));
981 assert!(
982 idle_free.is_expired(&entry, inserted + ttl),
983 "TTL is inclusive"
984 );
985 }
986
987 #[tokio::test]
990 async fn a_miss_does_not_read_the_clock() {
991 use wacore::time::clock_reads;
992
993 for cache in [
994 PortableCache::<String, u32>::builder()
995 .max_capacity(100)
996 .time_to_live(Duration::from_secs(60))
997 .build(),
998 PortableCache::<String, u32>::builder()
999 .max_capacity(100)
1000 .time_to_idle(Duration::from_secs(60))
1001 .build(),
1002 ] {
1003 cache.insert("present".into(), 1).await;
1004
1005 let base = clock_reads::snapshot();
1006 assert!(cache.get("absent").await.is_none());
1007 assert!(cache.remove("absent").await.is_none());
1008 assert_eq!(
1009 clock_reads::since(base).monotonic,
1010 0,
1011 "a miss must not read the monotonic clock"
1012 );
1013
1014 let hit = clock_reads::snapshot();
1015 assert_eq!(cache.get("present").await, Some(1));
1016 assert_eq!(
1017 clock_reads::since(hit).monotonic,
1018 1,
1019 "a hit reads once, to decide expiry"
1020 );
1021 }
1022 }
1023
1024 #[tokio::test]
1025 async fn test_ttl_expiry() {
1026 let cache: PortableCache<String, String> = PortableCache::builder()
1027 .max_capacity(100)
1028 .time_to_live(Duration::from_millis(50))
1029 .build();
1030
1031 cache.insert("key1".to_string(), "value1".to_string()).await;
1032 assert_eq!(cache.get("key1").await, Some("value1".to_string()));
1033
1034 tokio::time::sleep(Duration::from_millis(60)).await;
1035 assert!(cache.get("key1").await.is_none());
1036 }
1037
1038 #[tokio::test]
1039 async fn test_invalidate() {
1040 let cache = build_cache::<String, String>();
1041
1042 cache.insert("key1".to_string(), "value1".to_string()).await;
1043 cache.invalidate("key1").await;
1044 assert!(cache.get("key1").await.is_none());
1045 }
1046
1047 #[tokio::test]
1048 async fn test_invalidate_all() {
1049 let cache = build_cache::<String, u32>();
1050
1051 cache.insert("a".into(), 1).await;
1052 cache.insert("b".into(), 2).await;
1053 cache.invalidate_all();
1054 assert_eq!(cache.entry_count(), 0);
1055 assert!(cache.get("a").await.is_none());
1056 }
1057
1058 #[tokio::test]
1059 async fn test_remove() {
1060 let cache = build_cache::<String, String>();
1061
1062 cache.insert("key1".to_string(), "v1".to_string()).await;
1063 let removed = cache.remove("key1").await;
1064 assert_eq!(removed, Some("v1".to_string()));
1065 assert!(cache.get("key1").await.is_none());
1066 }
1067
1068 #[tokio::test]
1069 async fn test_iter_snapshot_includes_expired() {
1070 let cache: PortableCache<String, u32> = PortableCache::builder()
1074 .max_capacity(100)
1075 .time_to_live(Duration::from_millis(10))
1076 .build();
1077 cache.insert("a".to_string(), 1).await;
1078 cache.insert("b".to_string(), 2).await;
1079 tokio::time::sleep(Duration::from_millis(20)).await;
1080
1081 let mut keys: Vec<String> = cache.iter().map(|(k, _)| k.as_ref().clone()).collect();
1082 keys.sort();
1083 assert_eq!(keys, vec!["a".to_string(), "b".to_string()]);
1084 }
1085
1086 #[tokio::test]
1087 async fn test_get_with_basic() {
1088 let cache = build_cache::<String, u32>();
1089
1090 let v = cache.get_with("key1".to_string(), async { 42 }).await;
1091 assert_eq!(v, 42);
1092
1093 let v = cache.get_with("key1".to_string(), async { 99 }).await;
1094 assert_eq!(v, 42);
1095 }
1096
1097 #[tokio::test]
1098 async fn test_get_with_by_ref_basic() {
1099 let cache = build_cache::<String, u32>();
1100 let key = "key1".to_string();
1101
1102 let v = cache.get_with_by_ref(&key, async { 42 }).await;
1103 assert_eq!(v, 42);
1104
1105 let v = cache.get_with_by_ref(&key, async { 99 }).await;
1106 assert_eq!(v, 42);
1107 }
1108
1109 #[tokio::test]
1110 async fn test_get_with_single_flight() {
1111 let cache: PortableCache<String, Arc<AtomicUsize>> =
1112 PortableCache::builder().max_capacity(100).build();
1113
1114 let init_count = Arc::new(AtomicUsize::new(0));
1115 let num_tasks = 20;
1116 let barrier = Arc::new(tokio::sync::Barrier::new(num_tasks));
1117
1118 let mut handles = Vec::new();
1119 for _ in 0..num_tasks {
1120 let cache = cache.clone();
1121 let init_count = init_count.clone();
1122 let barrier = barrier.clone();
1123 handles.push(tokio::spawn(async move {
1124 barrier.wait().await;
1125 cache
1126 .get_with("shared_key".to_string(), async {
1127 init_count.fetch_add(1, Ordering::SeqCst);
1128 tokio::task::yield_now().await;
1129 Arc::new(AtomicUsize::new(0))
1130 })
1131 .await
1132 }));
1133 }
1134
1135 let mut results = Vec::new();
1136 for h in handles {
1137 results.push(h.await.unwrap());
1138 }
1139
1140 assert_eq!(init_count.load(Ordering::SeqCst), 1);
1141 let first = &results[0];
1142 for r in &results[1..] {
1143 assert!(Arc::ptr_eq(first, r));
1144 }
1145 }
1146
1147 #[tokio::test]
1148 async fn test_get_with_by_ref_single_flight() {
1149 let cache: PortableCache<String, Arc<AtomicUsize>> =
1150 PortableCache::builder().max_capacity(100).build();
1151
1152 let init_count = Arc::new(AtomicUsize::new(0));
1153 let num_tasks = 20;
1154 let barrier = Arc::new(tokio::sync::Barrier::new(num_tasks));
1155
1156 let mut handles = Vec::new();
1157 for _ in 0..num_tasks {
1158 let cache = cache.clone();
1159 let init_count = init_count.clone();
1160 let barrier = barrier.clone();
1161 handles.push(tokio::spawn(async move {
1162 barrier.wait().await;
1163 let key = "shared_key".to_string();
1164 cache
1165 .get_with_by_ref(&key, async {
1166 init_count.fetch_add(1, Ordering::SeqCst);
1167 tokio::task::yield_now().await;
1168 Arc::new(AtomicUsize::new(0))
1169 })
1170 .await
1171 }));
1172 }
1173
1174 let mut results = Vec::new();
1175 for h in handles {
1176 results.push(h.await.unwrap());
1177 }
1178
1179 assert_eq!(init_count.load(Ordering::SeqCst), 1);
1180 let first = &results[0];
1181 for r in &results[1..] {
1182 assert!(Arc::ptr_eq(first, r));
1183 }
1184 }
1185
1186 #[tokio::test]
1187 async fn test_get_with_different_keys_parallel() {
1188 let cache = build_cache::<String, u32>();
1189
1190 let init_count = Arc::new(AtomicUsize::new(0));
1191 let mut handles = Vec::new();
1192 for i in 0..10 {
1193 let cache = cache.clone();
1194 let init_count = init_count.clone();
1195 handles.push(tokio::spawn(async move {
1196 cache
1197 .get_with(format!("key_{i}"), async {
1198 init_count.fetch_add(1, Ordering::SeqCst);
1199 i as u32
1200 })
1201 .await
1202 }));
1203 }
1204
1205 for (i, h) in handles.into_iter().enumerate() {
1206 assert_eq!(h.await.unwrap(), i as u32);
1207 }
1208 assert_eq!(init_count.load(Ordering::SeqCst), 10);
1209 }
1210
1211 #[tokio::test]
1212 async fn test_session_lock_pattern() {
1213 let cache: PortableCache<String, Arc<async_lock::Mutex<()>>> =
1214 PortableCache::builder().max_capacity(100).build();
1215
1216 let counter = Arc::new(AtomicUsize::new(0));
1217 let num_tasks = 50;
1218 let barrier = Arc::new(tokio::sync::Barrier::new(num_tasks));
1219
1220 let mut handles = Vec::new();
1221 for _ in 0..num_tasks {
1222 let cache = cache.clone();
1223 let counter = counter.clone();
1224 let barrier = barrier.clone();
1225 handles.push(tokio::spawn(async move {
1226 barrier.wait().await;
1227 let mutex = cache
1228 .get_with("sender_123".to_string(), async {
1229 Arc::new(async_lock::Mutex::new(()))
1230 })
1231 .await;
1232 let _guard = mutex.lock().await;
1233 let val = counter.load(Ordering::SeqCst);
1234 tokio::task::yield_now().await;
1235 counter.store(val + 1, Ordering::SeqCst);
1236 }));
1237 }
1238
1239 for h in handles {
1240 h.await.unwrap();
1241 }
1242
1243 assert_eq!(counter.load(Ordering::SeqCst), num_tasks);
1244 }
1245
1246 #[tokio::test]
1247 async fn test_run_pending_tasks_cleans_expired() {
1248 let cache: PortableCache<String, u32> = PortableCache::builder()
1249 .max_capacity(100)
1250 .time_to_live(Duration::from_millis(50))
1251 .build();
1252
1253 cache.insert("a".into(), 1).await;
1254 cache.insert("b".into(), 2).await;
1255 assert_eq!(cache.entry_count(), 2);
1256
1257 tokio::time::sleep(Duration::from_millis(60)).await;
1258 cache.run_pending_tasks().await;
1259 assert_eq!(cache.entry_count(), 0);
1260 }
1261
1262 #[tokio::test]
1263 async fn test_get_with_reclaims_init_lock_eagerly() {
1264 let cache: PortableCache<String, u32> = PortableCache::builder().max_capacity(100).build();
1268
1269 let _ = cache.get_with("key1".to_string(), async { 1 }).await;
1270 let _ = cache.get_with_by_ref("key2", async { 2 }).await;
1271
1272 let locks = cache.init_locks.map.lock().await;
1273 assert!(
1274 locks.is_empty(),
1275 "init locks must be reclaimed after get_with"
1276 );
1277 }
1278
1279 #[tokio::test]
1280 async fn cancelled_get_with_reclaims_init_lock() {
1281 let cache = build_cache::<String, u32>();
1284 let task = tokio::spawn({
1285 let cache = cache.clone();
1286 async move {
1287 cache
1288 .get_with("stuck".to_string(), std::future::pending::<u32>())
1289 .await
1290 }
1291 });
1292
1293 let mut registered = false;
1295 for _ in 0..400 {
1296 if !cache.init_locks.map.lock().await.is_empty() {
1297 registered = true;
1298 break;
1299 }
1300 tokio::time::sleep(Duration::from_millis(5)).await;
1301 }
1302 assert!(registered, "in-flight get_with never registered its lock");
1303
1304 task.abort();
1305 let _ = task.await;
1306
1307 let mut reclaimed = false;
1310 for _ in 0..400 {
1311 if cache.init_locks.map.lock().await.is_empty() {
1312 reclaimed = true;
1313 break;
1314 }
1315 tokio::time::sleep(Duration::from_millis(5)).await;
1316 }
1317 assert!(reclaimed, "cancelled get_with leaked its init lock");
1318 }
1319
1320 #[tokio::test]
1321 async fn init_locks_collision_shares_one_lock() {
1322 let registry = InitLocks::new();
1325 let first = registry.acquire(42).await;
1326 let second = registry.acquire(42).await;
1327 assert!(
1328 Arc::ptr_eq(&first, &second),
1329 "same hash must yield the same init lock"
1330 );
1331
1332 registry.reclaim(42, &first).await;
1334 assert!(
1335 registry.map.lock().await.contains_key(&42),
1336 "reclaim must not drop a lock another caller still holds"
1337 );
1338
1339 drop(second);
1341 registry.reclaim(42, &first).await;
1342 assert!(
1343 registry.map.lock().await.is_empty(),
1344 "last reclaim must drop the registry entry"
1345 );
1346 }
1347
1348 #[derive(Clone, PartialEq, Eq, Debug)]
1351 struct CollidingKey(&'static str);
1352
1353 impl Hash for CollidingKey {
1354 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1355 state.write_u64(0);
1356 }
1357 }
1358
1359 #[tokio::test]
1360 async fn colliding_keys_keep_distinct_values() {
1361 let cache: PortableCache<CollidingKey, u32> =
1362 PortableCache::builder().max_capacity(16).build();
1363 let (a, b) = (CollidingKey("a"), CollidingKey("b"));
1364 assert_eq!(
1365 cache.init_locks.hash_of(&a),
1366 cache.init_locks.hash_of(&b),
1367 "test premise: both keys must share one init-lock slot"
1368 );
1369
1370 let barrier = Arc::new(tokio::sync::Barrier::new(2));
1373 let mut tasks = Vec::new();
1374 for (key, value) in [(a.clone(), 1u32), (b.clone(), 2u32)] {
1375 let cache = cache.clone();
1376 let barrier = barrier.clone();
1377 tasks.push(tokio::spawn(async move {
1378 barrier.wait().await;
1379 cache
1380 .get_with(key, async {
1381 tokio::task::yield_now().await;
1382 value
1383 })
1384 .await
1385 }));
1386 }
1387 let mut results = Vec::new();
1388 for task in tasks {
1389 results.push(task.await.unwrap());
1390 }
1391 assert_eq!(results, vec![1, 2], "each key must get its own init value");
1392 assert_eq!(cache.get(&a).await, Some(1));
1393 assert_eq!(cache.get(&b).await, Some(2));
1394 }
1395
1396 #[tokio::test]
1397 async fn get_with_distinct_keys_share_registry_correctly() {
1398 let cache = build_cache::<String, u32>();
1401 let a = cache.get_with("a".to_string(), async { 1 }).await;
1402 let b = cache.get_with("b".to_string(), async { 2 }).await;
1403 assert_eq!((a, b), (1, 2));
1404 assert_eq!(cache.get("a").await, Some(1));
1405 assert_eq!(cache.get("b").await, Some(2));
1406 }
1407}