1use anyhow::Result;
10use std::collections::{HashMap, VecDeque};
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant};
16
17#[derive(Debug, Clone)]
22pub struct ResolvedEntity {
23 pub type_name: String,
25 pub key_json: String,
27 pub data_json: String,
29 pub resolved_at: Instant,
31 pub ttl: Duration,
33}
34
35impl ResolvedEntity {
36 pub fn is_expired(&self) -> bool {
38 self.resolved_at.elapsed() >= self.ttl
39 }
40
41 pub fn remaining_ttl(&self) -> Duration {
43 self.ttl.saturating_sub(self.resolved_at.elapsed())
44 }
45}
46
47#[derive(Debug, Clone, Hash, PartialEq, Eq)]
49pub struct EntityCacheKey {
50 pub tenant_id: Option<String>,
52 pub type_name: String,
54 pub key_json: String,
56}
57
58impl EntityCacheKey {
59 pub fn new(
61 tenant_id: Option<&str>,
62 type_name: impl Into<String>,
63 key_json: impl Into<String>,
64 ) -> Self {
65 Self {
66 tenant_id: tenant_id.map(|s| s.to_string()),
67 type_name: type_name.into(),
68 key_json: key_json.into(),
69 }
70 }
71}
72
73struct EntityCacheStore {
75 entries: HashMap<EntityCacheKey, ResolvedEntity>,
76 lru: VecDeque<EntityCacheKey>,
77 max_entries: usize,
78 type_stats: HashMap<String, (u64, u64)>,
80}
81
82impl EntityCacheStore {
83 fn new(max_entries: usize) -> Self {
84 Self {
85 entries: HashMap::new(),
86 lru: VecDeque::new(),
87 max_entries,
88 type_stats: HashMap::new(),
89 }
90 }
91
92 fn insert(&mut self, key: EntityCacheKey, entity: ResolvedEntity) {
93 if self.entries.contains_key(&key) {
95 self.lru.retain(|k| k != &key);
96 }
97
98 self.entries.insert(key.clone(), entity);
99 self.lru.push_back(key);
100
101 while self.entries.len() > self.max_entries {
103 if let Some(oldest) = self.lru.pop_front() {
104 self.entries.remove(&oldest);
105 } else {
106 break;
107 }
108 }
109 }
110
111 fn get_mut(&mut self, key: &EntityCacheKey) -> Option<&mut ResolvedEntity> {
112 self.entries.get_mut(key)
113 }
114
115 fn touch(&mut self, key: &EntityCacheKey) {
116 self.lru.retain(|k| k != key);
117 self.lru.push_back(key.clone());
118 }
119
120 fn remove(&mut self, key: &EntityCacheKey) {
121 self.entries.remove(key);
122 self.lru.retain(|k| k != key);
123 }
124
125 fn evict_expired(&mut self) -> usize {
126 let expired: Vec<EntityCacheKey> = self
127 .entries
128 .iter()
129 .filter(|(_, e)| e.is_expired())
130 .map(|(k, _)| k.clone())
131 .collect();
132
133 let count = expired.len();
134 for key in expired {
135 self.remove(&key);
136 }
137 count
138 }
139
140 fn record_hit(&mut self, type_name: &str) {
141 let entry = self
142 .type_stats
143 .entry(type_name.to_string())
144 .or_insert((0, 0));
145 entry.0 += 1;
146 }
147
148 fn record_miss(&mut self, type_name: &str) {
149 let entry = self
150 .type_stats
151 .entry(type_name.to_string())
152 .or_insert((0, 0));
153 entry.1 += 1;
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct EntityCacheStats {
160 pub total_hits: u64,
162 pub total_misses: u64,
164 pub total_evictions: u64,
166 pub current_size: usize,
168 pub by_type: HashMap<String, (u64, u64)>,
170}
171
172impl EntityCacheStats {
173 pub fn hit_rate(&self) -> f64 {
175 let total = (self.total_hits + self.total_misses) as f64;
176 if total == 0.0 {
177 0.0
178 } else {
179 self.total_hits as f64 / total
180 }
181 }
182}
183
184pub struct EntityCache {
186 store: Arc<Mutex<EntityCacheStore>>,
187 default_ttl: Duration,
188 total_hits: Arc<AtomicU64>,
189 total_misses: Arc<AtomicU64>,
190 total_evictions: Arc<AtomicU64>,
191}
192
193impl std::fmt::Debug for EntityCache {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 f.debug_struct("EntityCache")
196 .field("default_ttl", &self.default_ttl)
197 .field("hits", &self.total_hits.load(Ordering::Relaxed))
198 .field("misses", &self.total_misses.load(Ordering::Relaxed))
199 .finish()
200 }
201}
202
203impl EntityCache {
204 pub fn new(max_entries: usize, default_ttl: Duration) -> Self {
206 Self {
207 store: Arc::new(Mutex::new(EntityCacheStore::new(max_entries))),
208 default_ttl,
209 total_hits: Arc::new(AtomicU64::new(0)),
210 total_misses: Arc::new(AtomicU64::new(0)),
211 total_evictions: Arc::new(AtomicU64::new(0)),
212 }
213 }
214
215 pub fn get(&self, key: &EntityCacheKey) -> Option<String> {
219 let mut store = self.store.lock().unwrap_or_else(|p| p.into_inner());
220
221 let entity_info: Option<(bool, String, String)> = store.get_mut(key).map(|entity| {
225 (
226 entity.is_expired(),
227 entity.type_name.clone(),
228 entity.data_json.clone(),
229 )
230 });
231
232 match entity_info {
233 Some((true, ref type_name, _)) => {
234 store.remove(key);
236 store.record_miss(type_name);
237 self.total_misses.fetch_add(1, Ordering::Relaxed);
238 self.total_evictions.fetch_add(1, Ordering::Relaxed);
239 None
240 }
241 Some((false, ref type_name, ref data)) => {
242 let data_clone = data.clone();
244 store.touch(key);
245 store.record_hit(type_name);
246 self.total_hits.fetch_add(1, Ordering::Relaxed);
247 Some(data_clone)
248 }
249 None => {
250 let type_name = key.type_name.clone();
251 store.record_miss(&type_name);
252 self.total_misses.fetch_add(1, Ordering::Relaxed);
253 None
254 }
255 }
256 }
257
258 pub fn put(&self, key: EntityCacheKey, entity: ResolvedEntity) {
260 self.put_with_ttl(key, entity, self.default_ttl);
261 }
262
263 pub fn put_with_ttl(&self, key: EntityCacheKey, mut entity: ResolvedEntity, ttl: Duration) {
265 entity.ttl = ttl;
266 entity.resolved_at = Instant::now();
267 if let Ok(mut store) = self.store.lock() {
268 store.insert(key, entity);
269 }
270 }
271
272 pub fn evict_expired(&self) -> usize {
274 let count = self
275 .store
276 .lock()
277 .map(|mut s| s.evict_expired())
278 .unwrap_or(0);
279 self.total_evictions
280 .fetch_add(count as u64, Ordering::Relaxed);
281 count
282 }
283
284 pub fn clear(&self) -> usize {
286 if let Ok(mut store) = self.store.lock() {
287 let count = store.entries.len();
288 store.entries.clear();
289 store.lru.clear();
290 count
291 } else {
292 0
293 }
294 }
295
296 pub fn size(&self) -> usize {
298 self.store.lock().map(|s| s.entries.len()).unwrap_or(0)
299 }
300
301 pub fn stats(&self) -> EntityCacheStats {
303 let by_type = self
304 .store
305 .lock()
306 .map(|s| s.type_stats.clone())
307 .unwrap_or_default();
308
309 EntityCacheStats {
310 total_hits: self.total_hits.load(Ordering::Relaxed),
311 total_misses: self.total_misses.load(Ordering::Relaxed),
312 total_evictions: self.total_evictions.load(Ordering::Relaxed),
313 current_size: self.size(),
314 by_type,
315 }
316 }
317
318 pub fn hit_rate(&self) -> f64 {
320 let hits = self.total_hits.load(Ordering::Relaxed) as f64;
321 let misses = self.total_misses.load(Ordering::Relaxed) as f64;
322 let total = hits + misses;
323 if total == 0.0 {
324 0.0
325 } else {
326 hits / total
327 }
328 }
329}
330
331#[derive(Debug)]
333pub struct EntityBatch {
334 pub type_name: String,
336 pub keys: Vec<String>,
338 pub tenant_id: Option<String>,
340}
341
342impl EntityBatch {
343 pub fn new(type_name: impl Into<String>, tenant_id: Option<&str>) -> Self {
345 Self {
346 type_name: type_name.into(),
347 keys: Vec::new(),
348 tenant_id: tenant_id.map(|s| s.to_string()),
349 }
350 }
351
352 pub fn add_key(&mut self, key_json: impl Into<String>) {
354 self.keys.push(key_json.into());
355 }
356
357 pub fn len(&self) -> usize {
359 self.keys.len()
360 }
361
362 pub fn is_empty(&self) -> bool {
364 self.keys.is_empty()
365 }
366}
367
368type BatchResolverFn = Box<
372 dyn Fn(EntityBatch) -> Pin<Box<dyn Future<Output = Result<Vec<(String, String)>>> + Send>>
373 + Send
374 + Sync,
375>;
376
377pub struct EntityBatchLoader {
382 cache: Arc<EntityCache>,
383 resolver: Arc<BatchResolverFn>,
384 max_batch_size: usize,
386 default_ttl: Duration,
387}
388
389impl std::fmt::Debug for EntityBatchLoader {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 f.debug_struct("EntityBatchLoader")
392 .field("max_batch_size", &self.max_batch_size)
393 .field("default_ttl", &self.default_ttl)
394 .finish()
395 }
396}
397
398impl EntityBatchLoader {
399 pub fn new(
401 cache: Arc<EntityCache>,
402 resolver: impl Fn(EntityBatch) -> Pin<Box<dyn Future<Output = Result<Vec<(String, String)>>> + Send>>
403 + Send
404 + Sync
405 + 'static,
406 max_batch_size: usize,
407 default_ttl: Duration,
408 ) -> Self {
409 Self {
410 cache,
411 resolver: Arc::new(Box::new(resolver)),
412 max_batch_size,
413 default_ttl,
414 }
415 }
416
417 pub async fn load_many(
421 &self,
422 type_name: &str,
423 key_jsons: Vec<String>,
424 tenant_id: Option<&str>,
425 ) -> Result<HashMap<String, String>> {
426 let mut result: HashMap<String, String> = HashMap::new();
427 let mut cache_misses: Vec<String> = Vec::new();
428
429 for key_json in &key_jsons {
431 let cache_key = EntityCacheKey::new(tenant_id, type_name, key_json.as_str());
432 if let Some(data) = self.cache.get(&cache_key) {
433 result.insert(key_json.clone(), data);
434 } else {
435 cache_misses.push(key_json.clone());
436 }
437 }
438
439 for chunk in cache_misses.chunks(self.max_batch_size) {
441 let mut batch = EntityBatch::new(type_name, tenant_id);
442 for key in chunk {
443 batch.add_key(key.clone());
444 }
445
446 let resolved = (self.resolver)(batch).await?;
447
448 for (key_json, data_json) in resolved {
449 let cache_key = EntityCacheKey::new(tenant_id, type_name, key_json.as_str());
450 let entity = ResolvedEntity {
451 type_name: type_name.to_string(),
452 key_json: key_json.clone(),
453 data_json: data_json.clone(),
454 resolved_at: Instant::now(),
455 ttl: self.default_ttl,
456 };
457 self.cache.put(cache_key, entity);
458 result.insert(key_json, data_json);
459 }
460 }
461
462 Ok(result)
463 }
464
465 pub async fn load_one(
467 &self,
468 type_name: &str,
469 key_json: &str,
470 tenant_id: Option<&str>,
471 ) -> Result<Option<String>> {
472 let mut results = self
473 .load_many(type_name, vec![key_json.to_string()], tenant_id)
474 .await?;
475 Ok(results.remove(key_json))
476 }
477
478 pub fn cache(&self) -> &Arc<EntityCache> {
480 &self.cache
481 }
482
483 pub fn invalidate_type(&self, type_name: &str) -> usize {
485 let mut to_remove: Vec<EntityCacheKey> = Vec::new();
489 if let Ok(store) = self.cache.store.lock() {
490 for key in store.entries.keys() {
491 if key.type_name == type_name {
492 to_remove.push(key.clone());
493 }
494 }
495 }
496 let count = to_remove.len();
497 if let Ok(mut store) = self.cache.store.lock() {
498 for key in &to_remove {
499 store.remove(key);
500 }
501 }
502 count
503 }
504}
505
506#[allow(clippy::type_complexity)]
510pub fn mock_batch_resolver(
511) -> impl Fn(EntityBatch) -> Pin<Box<dyn Future<Output = Result<Vec<(String, String)>>> + Send>>
512 + Send
513 + Sync
514 + 'static {
515 |batch: EntityBatch| {
516 Box::pin(async move {
517 let results: Vec<(String, String)> = batch
518 .keys
519 .into_iter()
520 .map(|k| {
521 let data = format!(r#"{{"id": {k}, "type": "{}"}}"#, batch.type_name);
522 (k, data)
523 })
524 .collect();
525 Ok(results)
526 })
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use std::sync::Arc;
534 use std::time::Duration;
535
536 fn make_cache() -> Arc<EntityCache> {
537 Arc::new(EntityCache::new(100, Duration::from_secs(60)))
538 }
539
540 fn entity(type_name: &str, key: &str, data: &str) -> ResolvedEntity {
541 ResolvedEntity {
542 type_name: type_name.to_string(),
543 key_json: key.to_string(),
544 data_json: data.to_string(),
545 resolved_at: Instant::now(),
546 ttl: Duration::from_secs(60),
547 }
548 }
549
550 fn ekey(type_name: &str, key: &str) -> EntityCacheKey {
551 EntityCacheKey::new(None, type_name, key)
552 }
553
554 fn ekey_tenant(tenant: &str, type_name: &str, key: &str) -> EntityCacheKey {
555 EntityCacheKey::new(Some(tenant), type_name, key)
556 }
557
558 #[test]
561 fn test_entity_cache_key_equality() {
562 let k1 = ekey("Product", r#"{"id":1}"#);
563 let k2 = ekey("Product", r#"{"id":1}"#);
564 assert_eq!(k1, k2);
565 }
566
567 #[test]
568 fn test_entity_cache_key_different_types() {
569 let k1 = ekey("Product", r#"{"id":1}"#);
570 let k2 = ekey("User", r#"{"id":1}"#);
571 assert_ne!(k1, k2);
572 }
573
574 #[test]
575 fn test_entity_cache_key_different_tenants() {
576 let k1 = ekey_tenant("acme", "Product", r#"{"id":1}"#);
577 let k2 = ekey_tenant("corp", "Product", r#"{"id":1}"#);
578 assert_ne!(k1, k2);
579 }
580
581 #[test]
582 fn test_entity_cache_key_none_vs_some_tenant() {
583 let k1 = ekey("Product", r#"{"id":1}"#);
584 let k2 = ekey_tenant("acme", "Product", r#"{"id":1}"#);
585 assert_ne!(k1, k2);
586 }
587
588 #[test]
591 fn test_resolved_entity_not_expired_immediately() {
592 let e = entity("Product", r#"{"id":1}"#, r#"{"name":"Foo"}"#);
593 assert!(!e.is_expired());
594 }
595
596 #[test]
597 fn test_resolved_entity_expired_after_ttl() {
598 let mut e = entity("Product", r#"{"id":1}"#, r#"{"name":"Foo"}"#);
599 e.ttl = Duration::from_nanos(1);
600 e.resolved_at = Instant::now();
601 std::thread::sleep(Duration::from_millis(5));
602 assert!(e.is_expired());
603 }
604
605 #[test]
606 fn test_resolved_entity_remaining_ttl() {
607 let e = entity("Product", r#"{"id":1}"#, r#"{"name":"Foo"}"#);
608 assert!(e.remaining_ttl() > Duration::ZERO);
609 }
610
611 #[test]
614 fn test_entity_cache_put_and_get() {
615 let cache = make_cache();
616 let key = ekey("Product", r#"{"id":1}"#);
617 let e = entity("Product", r#"{"id":1}"#, r#"{"name":"Widget"}"#);
618 cache.put(key.clone(), e);
619
620 let result = cache.get(&key);
621 assert_eq!(result.as_deref(), Some(r#"{"name":"Widget"}"#));
622 }
623
624 #[test]
625 fn test_entity_cache_miss_returns_none() {
626 let cache = make_cache();
627 let key = ekey("Product", r#"{"id":999}"#);
628 assert!(cache.get(&key).is_none());
629 }
630
631 #[test]
632 fn test_entity_cache_expired_entry_removed_on_get() {
633 let cache = make_cache();
634 let key = ekey("Product", r#"{"id":1}"#);
635 let mut e = entity("Product", r#"{"id":1}"#, r#"{"name":"Old"}"#);
636 e.ttl = Duration::from_nanos(1);
637 cache.put_with_ttl(key.clone(), e, Duration::from_nanos(1));
638
639 std::thread::sleep(Duration::from_millis(5));
640
641 assert!(cache.get(&key).is_none());
642 assert_eq!(cache.size(), 0);
643 }
644
645 #[test]
646 fn test_entity_cache_lru_eviction() {
647 let cache = Arc::new(EntityCache::new(2, Duration::from_secs(60)));
648
649 let k1 = ekey("T", "k1");
650 let k2 = ekey("T", "k2");
651 let k3 = ekey("T", "k3");
652
653 cache.put(k1.clone(), entity("T", "k1", "d1"));
654 cache.put(k2.clone(), entity("T", "k2", "d2"));
655 cache.get(&k1);
657 cache.put(k3.clone(), entity("T", "k3", "d3"));
659
660 assert_eq!(cache.size(), 2);
661 assert!(cache.get(&k2).is_none(), "k2 should have been evicted");
662 assert!(cache.get(&k1).is_some());
663 assert!(cache.get(&k3).is_some());
664 }
665
666 #[test]
667 fn test_entity_cache_evict_expired() {
668 let cache = Arc::new(EntityCache::new(100, Duration::from_nanos(1)));
669 let key = ekey("Product", "exp");
670 cache.put_with_ttl(
671 key,
672 entity("Product", "exp", "old"),
673 Duration::from_nanos(1),
674 );
675 std::thread::sleep(Duration::from_millis(5));
676
677 let removed = cache.evict_expired();
678 assert_eq!(removed, 1);
679 assert_eq!(cache.size(), 0);
680 }
681
682 #[test]
683 fn test_entity_cache_clear() {
684 let cache = make_cache();
685 cache.put(ekey("T", "k1"), entity("T", "k1", "d1"));
686 cache.put(ekey("T", "k2"), entity("T", "k2", "d2"));
687
688 let removed = cache.clear();
689 assert_eq!(removed, 2);
690 assert_eq!(cache.size(), 0);
691 }
692
693 #[test]
694 fn test_entity_cache_stats() {
695 let cache = make_cache();
696 let key = ekey("Product", "k1");
697 cache.put(key.clone(), entity("Product", "k1", "d1"));
698 cache.get(&key); cache.get(&ekey("Product", "miss")); let stats = cache.stats();
702 assert_eq!(stats.total_hits, 1);
703 assert_eq!(stats.total_misses, 1);
704 assert_eq!(stats.current_size, 1);
705 assert!((cache.hit_rate() - 0.5).abs() < f64::EPSILON);
706 }
707
708 #[test]
709 fn test_entity_cache_per_type_stats() {
710 let cache = make_cache();
711 let key = ekey("Product", "k1");
712 cache.put(key.clone(), entity("Product", "k1", "d1"));
713 cache.get(&key); cache.get(&ekey("User", "u1")); let stats = cache.stats();
717 let product_stats = stats.by_type.get("Product");
718 assert!(product_stats.is_some());
719 assert_eq!(product_stats.map(|(h, _)| *h).unwrap_or(0), 1);
721 }
722
723 #[test]
724 fn test_entity_cache_tenant_isolation() {
725 let cache = make_cache();
726 let k1 = ekey_tenant("acme", "Product", r#"{"id":1}"#);
727 let k2 = ekey_tenant("corp", "Product", r#"{"id":1}"#);
728
729 cache.put(
730 k1.clone(),
731 entity("Product", r#"{"id":1}"#, r#"{"name":"ACME Widget"}"#),
732 );
733 cache.put(
734 k2.clone(),
735 entity("Product", r#"{"id":1}"#, r#"{"name":"Corp Widget"}"#),
736 );
737
738 assert_ne!(cache.get(&k1), cache.get(&k2));
739 assert_eq!(cache.size(), 2);
740 }
741
742 #[test]
745 fn test_entity_batch_add_key() {
746 let mut batch = EntityBatch::new("Product", None);
747 assert!(batch.is_empty());
748
749 batch.add_key(r#"{"id":1}"#);
750 batch.add_key(r#"{"id":2}"#);
751 assert_eq!(batch.len(), 2);
752 assert!(!batch.is_empty());
753 }
754
755 #[test]
756 fn test_entity_batch_tenant() {
757 let batch = EntityBatch::new("Product", Some("acme"));
758 assert_eq!(batch.tenant_id.as_deref(), Some("acme"));
759 }
760
761 #[tokio::test]
764 async fn test_batch_loader_load_one_hit() {
765 let cache = make_cache();
766 let loader = EntityBatchLoader::new(
767 Arc::clone(&cache),
768 mock_batch_resolver(),
769 50,
770 Duration::from_secs(60),
771 );
772
773 let result = loader.load_one("Product", r#"{"id":1}"#, None).await;
774 assert!(result.is_ok());
775 assert!(result.expect("should succeed").is_some());
776 }
777
778 #[tokio::test]
779 async fn test_batch_loader_caches_result() {
780 let cache = make_cache();
781 let loader = EntityBatchLoader::new(
782 Arc::clone(&cache),
783 mock_batch_resolver(),
784 50,
785 Duration::from_secs(60),
786 );
787
788 let _ = loader.load_one("Product", r#"{"id":42}"#, None).await;
790 assert_eq!(cache.size(), 1);
791
792 let result = loader.load_one("Product", r#"{"id":42}"#, None).await;
794 assert!(result.expect("should succeed").is_some());
795 }
796
797 #[tokio::test]
798 async fn test_batch_loader_load_many() {
799 let cache = make_cache();
800 let loader = EntityBatchLoader::new(
801 Arc::clone(&cache),
802 mock_batch_resolver(),
803 50,
804 Duration::from_secs(60),
805 );
806
807 let keys = vec![
808 r#"{"id":1}"#.to_string(),
809 r#"{"id":2}"#.to_string(),
810 r#"{"id":3}"#.to_string(),
811 ];
812
813 let results = loader.load_many("Product", keys.clone(), None).await;
814 assert!(results.is_ok());
815 let map = results.expect("should succeed");
816 assert_eq!(map.len(), 3);
817 for key in &keys {
818 assert!(map.contains_key(key.as_str()));
819 }
820 }
821
822 #[tokio::test]
823 async fn test_batch_loader_invalidate_type() {
824 let cache = make_cache();
825 let loader = EntityBatchLoader::new(
826 Arc::clone(&cache),
827 mock_batch_resolver(),
828 50,
829 Duration::from_secs(60),
830 );
831
832 let _ = loader.load_one("Product", r#"{"id":1}"#, None).await;
833 let _ = loader.load_one("Product", r#"{"id":2}"#, None).await;
834 let _ = loader.load_one("User", r#"{"id":1}"#, None).await;
835
836 assert_eq!(cache.size(), 3);
837
838 let removed = loader.invalidate_type("Product");
839 assert_eq!(removed, 2);
840 assert_eq!(cache.size(), 1);
841 }
842
843 #[tokio::test]
844 async fn test_batch_loader_batches_large_requests() {
845 let cache = make_cache();
847 let loader = EntityBatchLoader::new(
848 Arc::clone(&cache),
849 mock_batch_resolver(),
850 2, Duration::from_secs(60),
852 );
853
854 let keys: Vec<String> = (1..=5).map(|i| format!(r#"{{"id":{i}}}"#)).collect();
855 let results = loader.load_many("Product", keys, None).await;
856 assert!(results.is_ok());
857 assert_eq!(results.expect("should succeed").len(), 5);
858 }
859
860 #[test]
861 fn test_entity_cache_size_after_multiple_puts() {
862 let cache = make_cache();
863 for i in 0..10 {
864 cache.put(
865 ekey("T", &format!("k{i}")),
866 entity("T", &format!("k{i}"), &format!("d{i}")),
867 );
868 }
869 assert_eq!(cache.size(), 10);
870 }
871
872 #[test]
873 fn test_entity_cache_hit_rate_zero_when_no_requests() {
874 let cache = make_cache();
875 assert_eq!(cache.hit_rate(), 0.0);
876 }
877}