1use crate::compression::{CompressionAlgorithm, Compressor};
45use serde::{Deserialize, Serialize};
46use std::cell::RefCell;
47use std::collections::HashMap;
48use std::path::PathBuf;
49use thiserror::Error;
50use tokio::fs;
51use tokio::io::{AsyncReadExt, AsyncWriteExt};
52
53#[derive(Debug, Error)]
55pub enum TieredCacheError {
56 #[error("IO error: {0}")]
57 Io(#[from] std::io::Error),
58
59 #[error("Key not found: {0}")]
60 KeyNotFound(String),
61
62 #[error("Tier full: {tier}")]
63 TierFull { tier: String },
64
65 #[error("Serialization error: {0}")]
66 Serialization(String),
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
71pub enum CacheTier {
72 L1 = 1,
74 L2 = 2,
76 L3 = 3,
78}
79
80impl CacheTier {
81 #[must_use]
83 #[inline]
84 pub const fn name(&self) -> &'static str {
85 match self {
86 Self::L1 => "L1-Memory",
87 Self::L2 => "L2-SSD",
88 Self::L3 => "L3-HDD",
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95struct CacheItemMetadata {
96 key: String,
97 size_bytes: u64,
98 tier: CacheTier,
99 access_count: u64,
100 last_access_ms: i64,
101 created_ms: i64,
102}
103
104impl CacheItemMetadata {
105 fn new(key: String, size_bytes: u64, tier: CacheTier) -> Self {
107 let now_ms = std::time::SystemTime::now()
108 .duration_since(std::time::UNIX_EPOCH)
109 .unwrap_or_default()
110 .as_millis() as i64;
111
112 Self {
113 key,
114 size_bytes,
115 tier,
116 access_count: 0,
117 last_access_ms: now_ms,
118 created_ms: now_ms,
119 }
120 }
121
122 fn record_access(&mut self) {
124 self.access_count += 1;
125 self.last_access_ms = std::time::SystemTime::now()
126 .duration_since(std::time::UNIX_EPOCH)
127 .unwrap_or_default()
128 .as_millis() as i64;
129 }
130
131 #[must_use]
133 #[inline]
134 const fn should_promote(&self, threshold: u64) -> bool {
135 self.access_count >= threshold
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct TieredCacheConfig {
142 pub l1_capacity_bytes: u64,
144 pub l2_capacity_bytes: u64,
146 pub l3_capacity_bytes: u64,
148 pub l2_path: PathBuf,
150 pub l3_path: PathBuf,
152 pub promotion_threshold: u64,
154 pub compression: CompressionAlgorithm,
156}
157
158impl Default for TieredCacheConfig {
159 fn default() -> Self {
160 Self {
161 l1_capacity_bytes: 100 * 1024 * 1024, l2_capacity_bytes: 1024 * 1024 * 1024, l3_capacity_bytes: 10 * 1024 * 1024 * 1024, l2_path: PathBuf::from("./cache/l2"),
165 l3_path: PathBuf::from("./cache/l3"),
166 promotion_threshold: 3,
167 compression: CompressionAlgorithm::Balanced, }
169 }
170}
171
172#[derive(Debug, Clone, Default)]
174pub struct TieredCacheStats {
175 pub l1_hits: u64,
177 pub l2_hits: u64,
179 pub l3_hits: u64,
181 pub misses: u64,
183 pub promotions_l2_to_l1: u64,
185 pub promotions_l3_to_l2: u64,
187 pub demotions_l1_to_l2: u64,
189 pub demotions_l2_to_l3: u64,
191 pub evictions: u64,
193}
194
195impl TieredCacheStats {
196 #[must_use]
198 #[inline]
199 pub fn l1_hit_rate(&self) -> f64 {
200 let total = self.l1_hits + self.l2_hits + self.l3_hits + self.misses;
201 if total == 0 {
202 0.0
203 } else {
204 self.l1_hits as f64 / total as f64
205 }
206 }
207
208 #[must_use]
210 #[inline]
211 pub fn overall_hit_rate(&self) -> f64 {
212 let hits = self.l1_hits + self.l2_hits + self.l3_hits;
213 let total = hits + self.misses;
214 if total == 0 {
215 0.0
216 } else {
217 hits as f64 / total as f64
218 }
219 }
220
221 #[must_use]
223 #[inline]
224 pub fn average_tier(&self) -> f64 {
225 let hits = self.l1_hits + self.l2_hits + self.l3_hits;
226 if hits == 0 {
227 0.0
228 } else {
229 (self.l1_hits as f64 + self.l2_hits as f64 * 2.0 + self.l3_hits as f64 * 3.0)
230 / hits as f64
231 }
232 }
233}
234
235pub struct TieredCache {
237 config: TieredCacheConfig,
238 l1: HashMap<String, Vec<u8>>,
240 metadata: HashMap<String, CacheItemMetadata>,
242 l1_used: u64,
244 l2_used: u64,
245 l3_used: u64,
246 stats: TieredCacheStats,
248 compressor: RefCell<Compressor>,
250}
251
252impl TieredCache {
253 pub async fn new(config: TieredCacheConfig) -> Result<Self, TieredCacheError> {
255 fs::create_dir_all(&config.l2_path).await?;
257 fs::create_dir_all(&config.l3_path).await?;
258
259 let compressor = RefCell::new(Compressor::new(config.compression));
260
261 Ok(Self {
262 compressor,
263 config,
264 l1: HashMap::new(),
265 metadata: HashMap::new(),
266 l1_used: 0,
267 l2_used: 0,
268 l3_used: 0,
269 stats: TieredCacheStats::default(),
270 })
271 }
272
273 pub async fn put(&mut self, key: String, data: Vec<u8>) -> Result<(), TieredCacheError> {
275 let size = data.len() as u64;
276
277 if let Some(old_meta) = self.metadata.get(&key) {
279 self.remove_from_tier(&key, old_meta.tier).await?;
280 }
281
282 if self.l1_used + size <= self.config.l1_capacity_bytes {
284 self.l1.insert(key.clone(), data);
285 self.l1_used += size;
286 self.metadata.insert(
287 key.clone(),
288 CacheItemMetadata::new(key, size, CacheTier::L1),
289 );
290 Ok(())
291 } else {
292 self.evict_from_l1().await?;
294 if self.l1_used + size <= self.config.l1_capacity_bytes {
295 self.l1.insert(key.clone(), data);
296 self.l1_used += size;
297 self.metadata.insert(
298 key.clone(),
299 CacheItemMetadata::new(key, size, CacheTier::L1),
300 );
301 Ok(())
302 } else {
303 self.place_in_l2(key, data, size).await
305 }
306 }
307 }
308
309 pub async fn get(&mut self, key: &str) -> Result<Option<Vec<u8>>, TieredCacheError> {
311 let (tier, should_promote) = if let Some(meta) = self.metadata.get_mut(key) {
313 meta.record_access();
314 let should_promote = meta.should_promote(self.config.promotion_threshold);
315 (meta.tier, should_promote)
316 } else {
317 self.stats.misses += 1;
318 return Ok(None);
319 };
320
321 match tier {
322 CacheTier::L1 => {
323 self.stats.l1_hits += 1;
324 Ok(self.l1.get(key).cloned())
325 }
326 CacheTier::L2 => {
327 self.stats.l2_hits += 1;
328 let data = self.read_from_l2(key).await?;
329
330 if should_promote {
332 self.promote_to_l1(key.to_string(), data.clone()).await?;
333 }
334
335 Ok(Some(data))
336 }
337 CacheTier::L3 => {
338 self.stats.l3_hits += 1;
339 let data = self.read_from_l3(key).await?;
340
341 if should_promote {
343 self.promote_to_l2(key.to_string(), data.clone()).await?;
344 }
345
346 Ok(Some(data))
347 }
348 }
349 }
350
351 pub async fn remove(&mut self, key: &str) -> Result<(), TieredCacheError> {
353 if let Some(meta) = self.metadata.remove(key) {
354 self.remove_from_tier(key, meta.tier).await?;
355 }
356 Ok(())
357 }
358
359 #[must_use]
361 #[inline]
362 pub const fn stats(&self) -> &TieredCacheStats {
363 &self.stats
364 }
365
366 #[must_use]
368 #[inline]
369 pub fn l1_usage_percent(&self) -> f64 {
370 if self.config.l1_capacity_bytes == 0 {
371 0.0
372 } else {
373 self.l1_used as f64 / self.config.l1_capacity_bytes as f64
374 }
375 }
376
377 pub async fn warm_with_data(
383 &mut self,
384 items: Vec<(String, Vec<u8>)>,
385 ) -> Result<usize, TieredCacheError> {
386 let mut warmed = 0;
387
388 for (key, data) in items {
389 if self.put(key, data).await.is_ok() {
390 warmed += 1;
391 }
392 }
393
394 Ok(warmed)
395 }
396
397 pub async fn warm_from_keys(&mut self, keys: &[String]) -> Result<usize, TieredCacheError> {
402 let mut warmed = 0;
403
404 for key in keys {
405 if let Ok(data) = self.read_from_l2(key).await {
407 if self.put(key.clone(), data).await.is_ok() {
408 warmed += 1;
409 continue;
410 }
411 }
412
413 if let Ok(data) = self.read_from_l3(key).await {
415 if self.put(key.clone(), data).await.is_ok() {
416 warmed += 1;
417 }
418 }
419 }
420
421 Ok(warmed)
422 }
423
424 #[must_use]
428 pub fn export_hot_keys(&self, limit: usize) -> Vec<String> {
429 let mut items: Vec<_> = self
430 .metadata
431 .iter()
432 .map(|(key, meta)| (key.clone(), meta.access_count))
433 .collect();
434
435 items.sort_by(|a, b| b.1.cmp(&a.1));
436
437 items.into_iter().take(limit).map(|(key, _)| key).collect()
438 }
439
440 #[must_use]
442 #[inline]
443 pub fn len(&self) -> usize {
444 self.metadata.len()
445 }
446
447 #[must_use]
449 #[inline]
450 pub fn is_empty(&self) -> bool {
451 self.metadata.is_empty()
452 }
453
454 async fn place_in_l2(
457 &mut self,
458 key: String,
459 data: Vec<u8>,
460 size: u64,
461 ) -> Result<(), TieredCacheError> {
462 if self.l2_used + size > self.config.l2_capacity_bytes {
463 self.evict_from_l2().await?;
464 }
465
466 if self.l2_used + size <= self.config.l2_capacity_bytes {
467 self.write_to_l2(&key, &data).await?;
468 self.l2_used += size;
469 self.metadata.insert(
470 key.clone(),
471 CacheItemMetadata::new(key, size, CacheTier::L2),
472 );
473 Ok(())
474 } else {
475 self.place_in_l3(key, data, size).await
476 }
477 }
478
479 async fn place_in_l3(
480 &mut self,
481 key: String,
482 data: Vec<u8>,
483 size: u64,
484 ) -> Result<(), TieredCacheError> {
485 if self.l3_used + size > self.config.l3_capacity_bytes {
486 self.evict_from_l3().await?;
487 }
488
489 if self.l3_used + size <= self.config.l3_capacity_bytes {
490 self.write_to_l3(&key, &data).await?;
491 self.l3_used += size;
492 self.metadata.insert(
493 key.clone(),
494 CacheItemMetadata::new(key, size, CacheTier::L3),
495 );
496 Ok(())
497 } else {
498 Err(TieredCacheError::TierFull {
499 tier: "L3".to_string(),
500 })
501 }
502 }
503
504 async fn evict_from_l1(&mut self) -> Result<(), TieredCacheError> {
505 let lru_key = self
507 .metadata
508 .iter()
509 .filter(|(_, meta)| meta.tier == CacheTier::L1)
510 .min_by_key(|(_, meta)| meta.last_access_ms)
511 .map(|(key, _)| key.clone());
512
513 if let Some(key) = lru_key {
514 if let Some(data) = self.l1.remove(&key) {
515 let size = self.metadata.get(&key).map(|m| m.size_bytes).unwrap_or(0);
517
518 self.l1_used -= size;
519 self.write_to_l2(&key, &data).await?;
521 self.l2_used += size;
522
523 if let Some(meta) = self.metadata.get_mut(&key) {
525 meta.tier = CacheTier::L2;
526 }
527
528 self.stats.demotions_l1_to_l2 += 1;
529 }
530 }
531
532 Ok(())
533 }
534
535 async fn evict_from_l2(&mut self) -> Result<(), TieredCacheError> {
536 let lru_key = self
538 .metadata
539 .iter()
540 .filter(|(_, meta)| meta.tier == CacheTier::L2)
541 .min_by_key(|(_, meta)| meta.last_access_ms)
542 .map(|(key, _)| key.clone());
543
544 if let Some(key) = lru_key {
545 let size = self.metadata.get(&key).map(|m| m.size_bytes).unwrap_or(0);
547
548 let data = self.read_from_l2(&key).await?;
549
550 self.l2_used -= size;
551 self.write_to_l3(&key, &data).await?;
553 self.l3_used += size;
554
555 if let Some(meta) = self.metadata.get_mut(&key) {
557 meta.tier = CacheTier::L3;
558 }
559
560 self.stats.demotions_l2_to_l3 += 1;
561
562 let _ = fs::remove_file(self.l2_path(&key)).await;
564 }
565
566 Ok(())
567 }
568
569 async fn evict_from_l3(&mut self) -> Result<(), TieredCacheError> {
570 let lru_key = self
572 .metadata
573 .iter()
574 .filter(|(_, meta)| meta.tier == CacheTier::L3)
575 .min_by_key(|(_, meta)| meta.last_access_ms)
576 .map(|(key, _)| key.clone());
577
578 if let Some(key) = lru_key {
579 if let Some(meta) = self.metadata.remove(&key) {
580 self.l3_used -= meta.size_bytes;
581 let _ = fs::remove_file(self.l3_path(&key)).await;
582 self.stats.evictions += 1;
583 }
584 }
585
586 Ok(())
587 }
588
589 async fn promote_to_l1(&mut self, key: String, data: Vec<u8>) -> Result<(), TieredCacheError> {
590 let (size, current_tier) = if let Some(meta) = self.metadata.get(&key) {
592 (meta.size_bytes, meta.tier)
593 } else {
594 return Ok(());
595 };
596
597 if current_tier == CacheTier::L1 {
599 return Ok(());
600 }
601
602 while self.l1_used + size > self.config.l1_capacity_bytes {
604 self.evict_from_l1().await?;
605 }
606
607 match current_tier {
609 CacheTier::L2 => {
610 self.l2_used -= size;
611 let _ = fs::remove_file(self.l2_path(&key)).await;
612 self.stats.promotions_l2_to_l1 += 1;
613 }
614 CacheTier::L3 => {
615 self.l3_used -= size;
616 let _ = fs::remove_file(self.l3_path(&key)).await;
617 }
618 CacheTier::L1 => return Ok(()), }
620
621 self.l1.insert(key.clone(), data);
623 self.l1_used += size;
624
625 if let Some(meta) = self.metadata.get_mut(&key) {
627 meta.tier = CacheTier::L1;
628 }
629
630 Ok(())
631 }
632
633 async fn promote_to_l2(&mut self, key: String, data: Vec<u8>) -> Result<(), TieredCacheError> {
634 let (size, current_tier) = if let Some(meta) = self.metadata.get(&key) {
636 (meta.size_bytes, meta.tier)
637 } else {
638 return Ok(());
639 };
640
641 if current_tier == CacheTier::L3 {
642 while self.l2_used + size > self.config.l2_capacity_bytes {
644 self.evict_from_l2().await?;
645 }
646
647 self.l3_used -= size;
649 let _ = fs::remove_file(self.l3_path(&key)).await;
650
651 self.write_to_l2(&key, &data).await?;
653 self.l2_used += size;
654
655 if let Some(meta) = self.metadata.get_mut(&key) {
657 meta.tier = CacheTier::L2;
658 }
659
660 self.stats.promotions_l3_to_l2 += 1;
661 }
662
663 Ok(())
664 }
665
666 async fn remove_from_tier(
667 &mut self,
668 key: &str,
669 tier: CacheTier,
670 ) -> Result<(), TieredCacheError> {
671 if let Some(meta) = self.metadata.get(key) {
672 match tier {
673 CacheTier::L1 => {
674 self.l1.remove(key);
675 self.l1_used -= meta.size_bytes;
676 }
677 CacheTier::L2 => {
678 let _ = fs::remove_file(self.l2_path(key)).await;
679 self.l2_used -= meta.size_bytes;
680 }
681 CacheTier::L3 => {
682 let _ = fs::remove_file(self.l3_path(key)).await;
683 self.l3_used -= meta.size_bytes;
684 }
685 }
686 }
687 Ok(())
688 }
689
690 fn l2_path(&self, key: &str) -> PathBuf {
691 self.config.l2_path.join(format!("{}.cache", key))
692 }
693
694 fn l3_path(&self, key: &str) -> PathBuf {
695 self.config.l3_path.join(format!("{}.cache", key))
696 }
697
698 async fn write_to_l2(&self, key: &str, data: &[u8]) -> Result<(), TieredCacheError> {
699 let path = self.l2_path(key);
700
701 let write_data = if !self.config.compression.is_none() {
703 self.compressor
704 .borrow_mut()
705 .compress(data)
706 .map_err(|e| TieredCacheError::Io(std::io::Error::other(e)))?
707 } else {
708 data.to_vec()
709 };
710
711 let mut file = fs::File::create(path).await?;
712 file.write_all(&write_data).await?;
713 file.sync_all().await?;
714 Ok(())
715 }
716
717 async fn write_to_l3(&self, key: &str, data: &[u8]) -> Result<(), TieredCacheError> {
718 let path = self.l3_path(key);
719
720 let write_data = if !self.config.compression.is_none() {
722 self.compressor
723 .borrow_mut()
724 .compress(data)
725 .map_err(|e| TieredCacheError::Io(std::io::Error::other(e)))?
726 } else {
727 data.to_vec()
728 };
729
730 let mut file = fs::File::create(path).await?;
731 file.write_all(&write_data).await?;
732 file.sync_all().await?;
733 Ok(())
734 }
735
736 async fn read_from_l2(&self, key: &str) -> Result<Vec<u8>, TieredCacheError> {
737 let path = self.l2_path(key);
738 let mut file = fs::File::open(path).await?;
739 let mut compressed_data = Vec::new();
740 file.read_to_end(&mut compressed_data).await?;
741
742 let data = if !self.config.compression.is_none() {
744 self.compressor
745 .borrow_mut()
746 .decompress(&compressed_data)
747 .map_err(|e| TieredCacheError::Io(std::io::Error::other(e)))?
748 } else {
749 compressed_data
750 };
751
752 Ok(data)
753 }
754
755 async fn read_from_l3(&self, key: &str) -> Result<Vec<u8>, TieredCacheError> {
756 let path = self.l3_path(key);
757 let mut file = fs::File::open(path).await?;
758 let mut compressed_data = Vec::new();
759 file.read_to_end(&mut compressed_data).await?;
760
761 let data = if !self.config.compression.is_none() {
763 self.compressor
764 .borrow_mut()
765 .decompress(&compressed_data)
766 .map_err(|e| TieredCacheError::Io(std::io::Error::other(e)))?
767 } else {
768 compressed_data
769 };
770
771 Ok(data)
772 }
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778 use tempfile::TempDir;
779
780 async fn create_test_cache() -> (TempDir, TieredCache) {
781 let temp_dir = TempDir::new().unwrap();
782 let config = TieredCacheConfig {
783 l1_capacity_bytes: 100,
784 l2_capacity_bytes: 200,
785 l3_capacity_bytes: 300,
786 l2_path: temp_dir.path().join("l2"),
787 l3_path: temp_dir.path().join("l3"),
788 promotion_threshold: 2,
789 compression: CompressionAlgorithm::None, };
791 let cache = TieredCache::new(config).await.unwrap();
792 (temp_dir, cache)
793 }
794
795 #[tokio::test]
796 async fn test_tiered_cache_creation() {
797 let (_temp, cache) = create_test_cache().await;
798 assert_eq!(cache.l1_used, 0);
799 assert_eq!(cache.l2_used, 0);
800 assert_eq!(cache.l3_used, 0);
801 }
802
803 #[tokio::test]
804 async fn test_put_and_get_l1() {
805 let (_temp, mut cache) = create_test_cache().await;
806
807 cache
808 .put("key1".to_string(), b"small".to_vec())
809 .await
810 .unwrap();
811
812 let data = cache.get("key1").await.unwrap();
813 assert_eq!(data, Some(b"small".to_vec()));
814 assert_eq!(cache.stats.l1_hits, 1);
815 }
816
817 #[tokio::test]
818 async fn test_automatic_demotion() {
819 let (_temp, mut cache) = create_test_cache().await;
820
821 cache.put("key1".to_string(), vec![1; 60]).await.unwrap();
823 cache.put("key2".to_string(), vec![2; 60]).await.unwrap();
824
825 assert!(cache.stats.demotions_l1_to_l2 >= 1);
827 }
828
829 #[tokio::test]
830 async fn test_promotion_on_access() {
831 let (_temp, mut cache) = create_test_cache().await;
832
833 cache.put("key1".to_string(), vec![1; 60]).await.unwrap();
835 cache.put("key2".to_string(), vec![2; 60]).await.unwrap();
836
837 let _ = cache.get("key1").await;
839 let _ = cache.get("key1").await;
840 let _ = cache.get("key1").await;
841
842 if let Some(meta) = cache.metadata.get("key1") {
844 assert_eq!(meta.tier, CacheTier::L1);
845 }
846 }
847
848 #[tokio::test]
849 async fn test_hit_rate_calculation() {
850 let (_temp, mut cache) = create_test_cache().await;
851
852 cache
853 .put("key1".to_string(), b"data".to_vec())
854 .await
855 .unwrap();
856
857 let _ = cache.get("key1").await;
858 let _ = cache.get("key1").await;
859 let _ = cache.get("nonexistent").await;
860
861 let hit_rate = cache.stats.overall_hit_rate();
862 assert!((hit_rate - 0.666).abs() < 0.01);
863 }
864
865 #[tokio::test]
866 async fn test_remove() {
867 let (_temp, mut cache) = create_test_cache().await;
868
869 cache
870 .put("key1".to_string(), b"data".to_vec())
871 .await
872 .unwrap();
873 assert!(cache.get("key1").await.unwrap().is_some());
874
875 cache.remove("key1").await.unwrap();
876 assert!(cache.get("key1").await.unwrap().is_none());
877 }
878
879 #[tokio::test]
880 async fn test_warm_with_data() {
881 let (_temp, mut cache) = create_test_cache().await;
882
883 let warm_data = vec![
884 ("key1".to_string(), b"data1".to_vec()),
885 ("key2".to_string(), b"data2".to_vec()),
886 ("key3".to_string(), b"data3".to_vec()),
887 ];
888
889 let warmed = cache.warm_with_data(warm_data).await.unwrap();
890 assert_eq!(warmed, 3);
891
892 assert!(cache.get("key1").await.unwrap().is_some());
893 assert!(cache.get("key2").await.unwrap().is_some());
894 assert!(cache.get("key3").await.unwrap().is_some());
895 }
896
897 #[tokio::test]
898 async fn test_warm_from_keys() {
899 let (_temp, mut cache) = create_test_cache().await;
900
901 cache.put("key1".to_string(), vec![0u8; 150]).await.unwrap();
903 cache.put("key2".to_string(), vec![0u8; 150]).await.unwrap();
904
905 let _metadata_before = cache.metadata.clone();
907
908 let config = TieredCacheConfig {
910 l1_capacity_bytes: 100,
911 l2_capacity_bytes: 200,
912 l3_capacity_bytes: 300,
913 l2_path: cache.config.l2_path.clone(),
914 l3_path: cache.config.l3_path.clone(),
915 promotion_threshold: 2,
916 compression: CompressionAlgorithm::None,
917 };
918 let mut new_cache = TieredCache::new(config).await.unwrap();
919
920 let keys = vec!["key1".to_string(), "key2".to_string()];
922 let _warmed = new_cache.warm_from_keys(&keys).await.unwrap();
923 }
925
926 #[tokio::test]
927 async fn test_export_hot_keys() {
928 let (_temp, mut cache) = create_test_cache().await;
929
930 cache
932 .put("hot1".to_string(), b"data".to_vec())
933 .await
934 .unwrap();
935 cache
936 .put("hot2".to_string(), b"data".to_vec())
937 .await
938 .unwrap();
939 cache
940 .put("cold".to_string(), b"data".to_vec())
941 .await
942 .unwrap();
943
944 for _ in 0..5 {
946 let _ = cache.get("hot1").await;
947 }
948 for _ in 0..3 {
949 let _ = cache.get("hot2").await;
950 }
951 let _ = cache.get("cold").await;
952
953 let hot_keys = cache.export_hot_keys(2);
955 assert_eq!(hot_keys.len(), 2);
956 assert!(hot_keys.contains(&"hot1".to_string()));
957 assert!(hot_keys.contains(&"hot2".to_string()));
958 }
959
960 #[tokio::test]
961 async fn test_len_and_is_empty() {
962 let (_temp, mut cache) = create_test_cache().await;
963
964 assert!(cache.is_empty());
965 assert_eq!(cache.len(), 0);
966
967 cache
968 .put("key1".to_string(), b"data".to_vec())
969 .await
970 .unwrap();
971 assert!(!cache.is_empty());
972 assert_eq!(cache.len(), 1);
973
974 cache
975 .put("key2".to_string(), b"data".to_vec())
976 .await
977 .unwrap();
978 assert_eq!(cache.len(), 2);
979
980 cache.remove("key1").await.unwrap();
981 assert_eq!(cache.len(), 1);
982 }
983}