Skip to main content

sample_app/
sample_app.rs

1//! Sample app: a tour of `subms-block-cache`, base API first, then each
2//! optional feature. Run the base with `cargo run --example sample_app`; add
3//! `--all-features` (or a subset like `--features arc`) to see the feature
4//! sections light up.
5//!
6//! The scenario throughout is a hot block cache in front of a cold columnar
7//! store - a market-data query engine reads fixed-size column blocks (pages)
8//! by id, and the cache serves repeat reads without paying the cold fetch.
9//!
10//! * base              - bounded page cache, clock-sweep eviction
11//! * arc               - scan-resistant cache that holds the frequent set
12//! * tinylfu           - frequency-gated admission against a one-shot scan
13//! * weighted          - byte-budgeted eviction for variable-size pages
14//! * concurrent-shards - many query threads reading pages without contending
15//! * metrics           - hit-ratio observability on the cache
16
17use subms_block_cache::BlockCache;
18
19fn main() {
20    base_page_cache();
21
22    #[cfg(feature = "arc")]
23    arc_scan_resistance();
24
25    #[cfg(feature = "tinylfu")]
26    tinylfu_admission();
27
28    #[cfg(feature = "weighted")]
29    weighted_byte_budget();
30
31    #[cfg(feature = "concurrent-shards")]
32    sharded_parallel_readers();
33
34    #[cfg(feature = "metrics")]
35    metrics_hit_ratio();
36}
37
38/// Stand-in for a cold columnar-store fetch: decompress one page.
39fn read_block(id: u64) -> String {
40    format!("page:{id}")
41}
42
43/// Base API: a bounded cache warms from the store on a miss and serves the hot
44/// working set on repeat reads. When full, admitting a new page evicts the
45/// coldest slot and hands it back for write-back or drop.
46fn base_page_cache() {
47    println!("== base: block cache in front of a cold columnar store ==");
48    const CAP: usize = 4;
49    let mut cache: BlockCache<u64, String> = BlockCache::with_capacity(CAP);
50
51    let hot = [100u64, 101, 102, 103];
52    let mut cold_reads = 0;
53    for &id in &hot {
54        assert!(cache.get(&id).is_none(), "cold on first touch");
55        cache.put(id, read_block(id));
56        cold_reads += 1;
57    }
58    println!("  warmed {} pages, {cold_reads} cold reads", cache.len());
59    assert_eq!(cache.len(), CAP);
60
61    let mut served = 0;
62    for &id in &hot {
63        assert!(cache.get(&id).is_some(), "resident page must never miss");
64        served += 1;
65    }
66    println!("  re-read the hot set: {served} served, 0 cold reads");
67    assert_eq!(served, CAP);
68
69    let (victim, _) = cache
70        .put(200, read_block(200))
71        .expect("full cache must evict to admit a new page");
72    println!("  admitted page 200, evicted page {victim}");
73    assert_eq!(cache.len(), CAP, "capacity is a hard bound");
74
75    // A compaction rewrote page 200, so the cached copy is stale. Invalidate
76    // it rather than waiting for the hand to come round.
77    assert!(cache.remove(&200).is_some(), "stale page must be dropped");
78    println!("  invalidated page 200, {} pages resident", cache.len());
79    cache.clear();
80    assert!(cache.is_empty(), "clear drops the whole segment");
81    println!("  segment dropped, cache empty, capacity still {CAP}");
82}
83
84/// `arc` feature: clock-sweep churns the whole cache under a scan. ARC keeps a
85/// frequently-read set (its T2 list) alive because a one-shot scan only ever
86/// lands in T1 and is evicted to the B1 ghost list without touching T2.
87#[cfg(feature = "arc")]
88fn arc_scan_resistance() {
89    use subms_block_cache::ArcCache;
90    println!("\n== arc: scan-resistant page cache ==");
91    let mut cache: ArcCache<u64, String> = ArcCache::with_capacity(8);
92    for id in 0u64..4 {
93        cache.put(id, read_block(id));
94        let _ = cache.get(&id); // second touch promotes into the frequent list
95    }
96    println!("  frequent set in T2: {} pages", cache.t2_len());
97
98    for id in 1000u64..1200 {
99        cache.put(id, read_block(id));
100    }
101    let survivors = (0u64..4).filter(|id| cache.get(id).is_some()).count();
102    println!("  hot pages surviving a 200-page scan: {survivors}/4");
103    assert_eq!(
104        survivors, 4,
105        "ARC must hold the frequent set through a scan"
106    );
107}
108
109/// `tinylfu` feature: a W-TinyLFU admission filter makes a scan page fight the
110/// resident it would displace, using a count-min-sketch of access frequency.
111/// Low-frequency one-shot pages lose the fight and never enter the cache.
112#[cfg(feature = "tinylfu")]
113fn tinylfu_admission() {
114    use subms_block_cache::TinyLfuCache;
115    println!("\n== tinylfu: frequency-gated admission ==");
116    let mut cache: TinyLfuCache<u64, String> = TinyLfuCache::with_capacity(64);
117    for _ in 0..50 {
118        for id in 0u64..16 {
119            let _ = cache.get(&id);
120        }
121    }
122    for id in 0u64..16 {
123        cache.put(id, read_block(id));
124    }
125    for _ in 0..50 {
126        for id in 0u64..16 {
127            let _ = cache.get(&id);
128        }
129    }
130
131    let rej_before = cache.rejections();
132    for id in 1000u64..3000 {
133        cache.put(id, read_block(id));
134    }
135    let rejected = cache.rejections() - rej_before;
136    println!(
137        "  admissions {}, scan pages rejected {rejected}",
138        cache.admissions()
139    );
140    assert!(
141        rejected > 0,
142        "admission filter must reject some one-shot scan pages"
143    );
144}
145
146/// `weighted` feature: pages vary in compressed size, so the budget is bytes,
147/// not slots. A `put` evicts until the new page fits; a page larger than the
148/// whole budget is handed straight back rather than flushing the resident set.
149#[cfg(feature = "weighted")]
150fn weighted_byte_budget() {
151    use subms_block_cache::WeightedCache;
152    println!("\n== weighted: byte-budgeted page cache ==");
153    let mut cache: WeightedCache<u64, Vec<u8>> =
154        WeightedCache::with_capacity_bytes(4096, |page: &Vec<u8>| page.len());
155
156    let mut evicted_total = 0;
157    for id in 0u64..64 {
158        let size = 128 + (id as usize % 8) * 128;
159        evicted_total += cache.put(id, vec![0u8; size]).len();
160    }
161    println!(
162        "  used {} / 4096 bytes, {} pages, {evicted_total} evicted",
163        cache.used_bytes(),
164        cache.len()
165    );
166    assert!(cache.used_bytes() <= 4096, "byte budget is a hard bound");
167
168    let too_big = cache.put(999, vec![0u8; 8192]);
169    assert_eq!(too_big.len(), 1, "oversized page is rejected, not admitted");
170    assert!(cache.get(&999).is_none());
171}
172
173/// `concurrent-shards` feature: the keyspace splits across independent shards
174/// by hash, each behind its own lock, so query threads touching different
175/// shards never block each other.
176#[cfg(feature = "concurrent-shards")]
177fn sharded_parallel_readers() {
178    use std::sync::Arc;
179    use std::thread;
180    use subms_block_cache::ShardedCache;
181    println!("\n== concurrent-shards: parallel query threads ==");
182    let cache: Arc<ShardedCache<u64, u64>> = Arc::new(ShardedCache::with_capacity(1024, 8));
183    println!("  {} shards", cache.num_shards());
184    for id in 0u64..256 {
185        cache.put(id, id);
186    }
187
188    let mut handles = Vec::new();
189    for _ in 0..4 {
190        let c = Arc::clone(&cache);
191        handles.push(thread::spawn(move || {
192            for id in 0u64..4000 {
193                let _ = c.get(&(id % 256));
194            }
195        }));
196    }
197    for t in 0u64..2 {
198        let c = Arc::clone(&cache);
199        handles.push(thread::spawn(move || {
200            for i in 0u64..2000 {
201                c.put(t * 10_000 + i, i);
202            }
203        }));
204    }
205    for h in handles {
206        h.join().unwrap();
207    }
208    println!("  survived concurrent load, {} pages resident", cache.len());
209    assert!(cache.len() <= 1024, "capacity holds across all shards");
210}
211
212/// `metrics` feature: per-instance hit / miss counters and a `hit_ratio()`
213/// helper, the number you actually watch to decide whether the cache is
214/// earning its footprint in front of the store.
215#[cfg(feature = "metrics")]
216fn metrics_hit_ratio() {
217    use subms_block_cache::MetricsCache;
218    println!("\n== metrics: hit-ratio observability ==");
219    let mut cache: MetricsCache<u64, String> = MetricsCache::with_capacity(4);
220    for id in 0u64..4 {
221        cache.put(id, read_block(id));
222    }
223    let _ = cache.get(&0);
224    let _ = cache.get(&1);
225    let _ = cache.get(&2);
226    let _ = cache.get(&99);
227    let m = cache.metrics();
228    println!(
229        "  hits {}, misses {}, hit ratio {:.2}",
230        m.hits(),
231        m.misses(),
232        m.hit_ratio()
233    );
234    assert_eq!(m.hits(), 3);
235    assert_eq!(m.misses(), 1);
236    assert!((m.hit_ratio() - 0.75).abs() < 1e-9);
237}