summaryrefslogtreecommitdiff
path: root/examples/hierarchical_cache_demo.rs
blob: ea3be89fa0638b54141122c512b5a90e8890877e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! Hierarchical Binary-Indexed Cache System Demo
//! 
//! This example demonstrates the multi-level cache hierarchy:
//! - L1: Fast in-memory LRU cache (configurable size)
//! - L2: Binary-indexed disk cache with 256 SHA1-based buckets
//! - L3: Remote package registry fallback (simulated)

use std::time::Instant;
use spandx::cache::Cache;
use camino::Utf8PathBuf;
use tempfile::TempDir;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("šŸ”— Hierarchical Binary-Indexed Cache System Demo");
    println!("================================================");
    
    // Initialize cache with small L1 cache for demonstration
    let temp_dir = TempDir::new()?;
    let cache_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf())
        .map_err(|e| format!("Failed to convert path: {:?}", e))?;
    let mut cache = Cache::with_memory_cache_size(cache_dir, 3); // Only 3 entries in L1
    
    println!("šŸ“Š Initial cache state:");
    print_cache_stats(&cache);
    
    // Simulate populating cache with package license data
    let packages = [
        ("rails", "7.0.0", "rubygems", vec!["MIT".to_string()]),
        ("express", "4.18.0", "npm", vec!["MIT".to_string()]),
        ("django", "4.2.0", "pypi", vec!["BSD-3-Clause".to_string()]),
        ("spring-boot", "3.0.0", "maven", vec!["Apache-2.0".to_string()]),
        ("react", "18.0.0", "npm", vec!["MIT".to_string()]),
        ("numpy", "1.24.0", "pypi", vec!["BSD-3-Clause".to_string()]),
    ];
    
    println!("\nšŸ”„ Populating cache with {} packages...", packages.len());
    for (name, version, pm, licenses) in &packages {
        cache.set_licenses(name, version, pm, licenses.clone()).await?;
        println!("  āœ… Cached {}@{} ({}): {:?}", name, version, pm, licenses);
    }
    
    println!("\nšŸ“Š Cache state after population:");
    print_cache_stats(&cache);
    
    // Demonstrate L1 cache hits (fastest)
    println!("\n⚔ Testing L1 cache hits (should be very fast):");
    for (name, version, pm, expected) in packages.iter().take(3) {
        let start = Instant::now();
        let result = cache.get_licenses(name, version, pm).await?;
        let duration = start.elapsed();
        
        match result {
            Some(licenses) => {
                println!("  šŸŽÆ L1 HIT: {}@{} -> {:?} ({:.2}μs)", 
                    name, version, licenses, duration.as_micros());
                assert_eq!(licenses, *expected);
            }
            None => println!("  āŒ MISS: {}@{}", name, version),
        }
    }
    
    // Clear L1 cache to test L2 fallback
    println!("\n🧹 Clearing L1 cache to demonstrate L2 fallback...");
    cache.clear_memory_cache();
    print_cache_stats(&cache);
    
    // Demonstrate L2 cache hits (slower but still fast)
    println!("\nšŸ’¾ Testing L2 cache hits (binary-indexed disk):");
    for (name, version, pm, expected) in &packages {
        let start = Instant::now();
        let result = cache.get_licenses(name, version, pm).await?;
        let duration = start.elapsed();
        
        match result {
            Some(licenses) => {
                println!("  šŸŽÆ L2 HIT: {}@{} -> {:?} ({:.2}μs)", 
                    name, version, licenses, duration.as_micros());
                assert_eq!(licenses, *expected);
            }
            None => println!("  āŒ MISS: {}@{}", name, version),
        }
    }
    
    println!("\nšŸ“Š Final cache state (L2 entries promoted to L1):");
    print_cache_stats(&cache);
    
    // Demonstrate cache miss (would trigger L3 fallback in real system)
    println!("\nšŸ” Testing cache miss (would trigger remote registry lookup):");
    let start = Instant::now();
    let result = cache.get_licenses("nonexistent", "1.0.0", "npm").await?;
    let duration = start.elapsed();
    
    match result {
        Some(licenses) => println!("  šŸŽÆ Unexpected hit: {:?}", licenses),
        None => println!("  āŒ MISS: nonexistent@1.0.0 -> would fetch from registry ({:.2}μs)", 
            duration.as_micros()),
    }
    
    // Demonstrate bucket distribution
    println!("\nšŸ—‚ļø  Cache bucket distribution:");
    print_bucket_analysis(&packages);
    
    println!("\n✨ Demo complete! The hierarchical cache provides:");
    println!("   • L1: Ultra-fast memory access (μs latency)");
    println!("   • L2: Fast binary-indexed disk access (ms latency)");
    println!("   • L3: Remote registry fallback (s latency, not shown)");
    println!("   • Automatic promotion between levels");
    println!("   • LRU eviction in L1 memory cache");
    println!("   • SHA1-based bucketing for optimal distribution");
    
    Ok(())
}

fn print_cache_stats(cache: &Cache) {
    let stats = cache.memory_cache_stats();
    println!("  L1 Memory Cache: {}/{} entries ({:.1}% utilization, {} remaining)",
        stats.entries, 
        stats.max_entries,
        stats.utilization() * 100.0,
        stats.remaining_capacity());
}

fn print_bucket_analysis(packages: &[(&str, &str, &str, Vec<String>)]) {
    use sha1::{Digest, Sha1};
    
    for (name, version, pm, _) in packages {
        let mut hasher = Sha1::new();
        hasher.update(name.as_bytes());
        let hash = hasher.finalize();
        let bucket = format!("{:02x}", hash[0]);
        
        println!("  šŸ“ {}@{} ({}) -> bucket {} (hash: {:02x}{}...)",
            name, version, pm, bucket, hash[0], 
            hash.iter().skip(1).take(2).map(|b| format!("{:02x}", b)).collect::<String>());
    }
}