Skip to main content

somatize_runtime/cache/
tiered.rs

1use somatize_core::cache::{CacheKey, CacheStore, CacheTier, EntryMeta};
2use somatize_core::error::Result;
3use somatize_core::value::Value;
4
5/// Multi-level cache with automatic promotion.
6///
7/// Checks tiers in order: Memory → Local → (Remote, future).
8/// On a hit in a lower tier, promotes the entry to faster tiers.
9pub struct TieredCache {
10    tiers: Vec<(CacheTier, Box<dyn CacheStore>)>,
11}
12
13impl TieredCache {
14    /// Create a tiered cache from a list of (tier, store) pairs.
15    /// Tiers should be ordered fastest-first.
16    pub fn new(tiers: Vec<(CacheTier, Box<dyn CacheStore>)>) -> Self {
17        Self { tiers }
18    }
19
20    /// Create a two-level cache: Memory + Local.
21    pub fn memory_and_local(memory: Box<dyn CacheStore>, local: Box<dyn CacheStore>) -> Self {
22        Self {
23            tiers: vec![(CacheTier::Memory, memory), (CacheTier::Local, local)],
24        }
25    }
26}
27
28impl CacheStore for TieredCache {
29    fn get(&self, key: &CacheKey) -> Result<Option<Value>> {
30        for (i, (_, store)) in self.tiers.iter().enumerate() {
31            if let Some(value) = store.get(key)? {
32                // Promote to faster tiers
33                for (_, faster_store) in &self.tiers[..i] {
34                    let _ = faster_store.put(key, &value);
35                }
36                return Ok(Some(value));
37            }
38        }
39        Ok(None)
40    }
41
42    fn put(&self, key: &CacheKey, value: &Value) -> Result<()> {
43        // Write to all tiers
44        for (_, store) in &self.tiers {
45            store.put(key, value)?;
46        }
47        Ok(())
48    }
49
50    fn exists(&self, key: &CacheKey) -> Result<bool> {
51        for (_, store) in &self.tiers {
52            if store.exists(key)? {
53                return Ok(true);
54            }
55        }
56        Ok(false)
57    }
58
59    fn remove(&self, key: &CacheKey) -> Result<()> {
60        for (_, store) in &self.tiers {
61            store.remove(key)?;
62        }
63        Ok(())
64    }
65
66    fn metadata(&self, key: &CacheKey) -> Result<Option<EntryMeta>> {
67        for (_, store) in &self.tiers {
68            if let Some(meta) = store.metadata(key)? {
69                return Ok(Some(meta));
70            }
71        }
72        Ok(None)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::cache::local::LocalCache;
80    use crate::cache::memory::MemoryCache;
81    use std::env;
82    use std::fs;
83    use std::path::PathBuf;
84
85    use std::sync::atomic::{AtomicU64, Ordering};
86    static COUNTER: AtomicU64 = AtomicU64::new(0);
87
88    fn temp_dir() -> PathBuf {
89        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
90        let dir = env::temp_dir().join(format!("soma_tiered_test_{}_{id}", std::process::id()));
91        let _ = fs::remove_dir_all(&dir);
92        dir
93    }
94
95    fn make_tiered() -> (TieredCache, PathBuf) {
96        let dir = temp_dir();
97        let memory = Box::new(MemoryCache::default());
98        let local = Box::new(LocalCache::new(&dir).unwrap());
99        (TieredCache::memory_and_local(memory, local), dir)
100    }
101
102    #[test]
103    fn put_writes_to_all_tiers() {
104        let (cache, dir) = make_tiered();
105        let key = CacheKey::hash_data(b"test");
106        let value = Value::tensor(vec![1.0, 2.0], vec![2]);
107
108        cache.put(&key, &value).unwrap();
109
110        // Both tiers should have it
111        assert!(cache.tiers[0].1.exists(&key).unwrap()); // memory
112        assert!(cache.tiers[1].1.exists(&key).unwrap()); // local
113
114        let _ = fs::remove_dir_all(&dir);
115    }
116
117    #[test]
118    fn get_from_memory_first() {
119        let (cache, dir) = make_tiered();
120        let key = CacheKey::hash_data(b"test");
121        let value = Value::tensor(vec![1.0], vec![1]);
122
123        cache.put(&key, &value).unwrap();
124        let result = cache.get(&key).unwrap().unwrap();
125        assert_eq!(result, value);
126
127        let _ = fs::remove_dir_all(&dir);
128    }
129
130    #[test]
131    fn promotes_from_local_to_memory() {
132        let dir = temp_dir();
133        let memory = Box::new(MemoryCache::default());
134        let local = Box::new(LocalCache::new(&dir).unwrap());
135
136        // Write only to local
137        let key = CacheKey::hash_data(b"local_only");
138        let value = Value::tensor(vec![42.0], vec![1]);
139        local.put(&key, &value).unwrap();
140
141        let tiered = TieredCache::memory_and_local(memory, local);
142
143        // Memory doesn't have it
144        assert!(!tiered.tiers[0].1.exists(&key).unwrap());
145
146        // Get should find it in local and promote to memory
147        let result = tiered.get(&key).unwrap().unwrap();
148        assert_eq!(result, value);
149
150        // Now memory should have it
151        assert!(tiered.tiers[0].1.exists(&key).unwrap());
152
153        let _ = fs::remove_dir_all(&dir);
154    }
155
156    #[test]
157    fn miss_returns_none() {
158        let (cache, dir) = make_tiered();
159        assert!(cache.get(&CacheKey::hash_data(b"nope")).unwrap().is_none());
160        let _ = fs::remove_dir_all(&dir);
161    }
162
163    #[test]
164    fn remove_from_all_tiers() {
165        let (cache, dir) = make_tiered();
166        let key = CacheKey::hash_data(b"test");
167        cache.put(&key, &Value::Empty).unwrap();
168        cache.remove(&key).unwrap();
169
170        assert!(!cache.tiers[0].1.exists(&key).unwrap());
171        assert!(!cache.tiers[1].1.exists(&key).unwrap());
172
173        let _ = fs::remove_dir_all(&dir);
174    }
175}