somatize_runtime/cache/
tiered.rs1use somatize_core::cache::{CacheKey, CacheStore, CacheTier, EntryMeta};
4use somatize_core::error::Result;
5use somatize_core::value::Value;
6
7pub struct TieredCache {
12 tiers: Vec<(CacheTier, Box<dyn CacheStore>)>,
13}
14
15impl TieredCache {
16 pub fn new(tiers: Vec<(CacheTier, Box<dyn CacheStore>)>) -> Self {
19 Self { tiers }
20 }
21
22 pub fn memory_and_local(memory: Box<dyn CacheStore>, local: Box<dyn CacheStore>) -> Self {
24 Self {
25 tiers: vec![(CacheTier::Memory, memory), (CacheTier::Local, local)],
26 }
27 }
28}
29
30impl CacheStore for TieredCache {
31 fn tier(&self) -> CacheTier {
33 self.tiers
34 .first()
35 .map(|(t, _)| *t)
36 .unwrap_or(CacheTier::Memory)
37 }
38
39 fn get(&self, key: &CacheKey) -> Result<Option<Value>> {
40 Ok(self.get_located(key)?.map(|(value, _)| value))
41 }
42
43 fn get_located(&self, key: &CacheKey) -> Result<Option<(Value, CacheTier)>> {
47 for (i, (tier, store)) in self.tiers.iter().enumerate() {
48 if let Some(value) = store.get(key)? {
49 for (_, faster_store) in &self.tiers[..i] {
51 let _ = faster_store.put(key, &value);
52 }
53 return Ok(Some((value, *tier)));
54 }
55 }
56 Ok(None)
57 }
58
59 fn put(&self, key: &CacheKey, value: &Value) -> Result<()> {
60 for (_, store) in &self.tiers {
62 store.put(key, value)?;
63 }
64 Ok(())
65 }
66
67 fn put_with_origin(
68 &self,
69 key: &CacheKey,
70 value: &Value,
71 origin: &somatize_core::cache::Origin,
72 ) -> Result<()> {
73 for (_, store) in &self.tiers {
74 store.put_with_origin(key, value, origin)?;
75 }
76 Ok(())
77 }
78
79 fn exists(&self, key: &CacheKey) -> Result<bool> {
80 for (_, store) in &self.tiers {
81 if store.exists(key)? {
82 return Ok(true);
83 }
84 }
85 Ok(false)
86 }
87
88 fn remove(&self, key: &CacheKey) -> Result<()> {
89 for (_, store) in &self.tiers {
90 store.remove(key)?;
91 }
92 Ok(())
93 }
94
95 fn metadata(&self, key: &CacheKey) -> Result<Option<EntryMeta>> {
96 for (_, store) in &self.tiers {
97 if let Some(meta) = store.metadata(key)? {
98 return Ok(Some(meta));
99 }
100 }
101 Ok(None)
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::cache::local::LocalCache;
109 use crate::cache::memory::MemoryCache;
110 use std::env;
111 use std::fs;
112 use std::path::PathBuf;
113
114 use std::sync::atomic::{AtomicU64, Ordering};
115 static COUNTER: AtomicU64 = AtomicU64::new(0);
116
117 fn temp_dir() -> PathBuf {
118 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
119 let dir = env::temp_dir().join(format!("soma_tiered_test_{}_{id}", std::process::id()));
120 let _ = fs::remove_dir_all(&dir);
121 dir
122 }
123
124 fn make_tiered() -> (TieredCache, PathBuf) {
125 let dir = temp_dir();
126 let memory = Box::new(MemoryCache::default());
127 let local = Box::new(LocalCache::new(&dir).unwrap());
128 (TieredCache::memory_and_local(memory, local), dir)
129 }
130
131 #[test]
135 fn get_located_names_the_tier_that_answered() {
136 let (cache, dir) = make_tiered();
137 let key = CacheKey::hash_data(b"only-on-disk");
138 let value = Value::tensor(vec![7.0], vec![1]);
139
140 let local = LocalCache::new(&dir).unwrap();
142 local.put(&key, &value).unwrap();
143
144 let (got, tier) = cache.get_located(&key).unwrap().unwrap();
145 assert_eq!(got, value);
146 assert_eq!(tier, CacheTier::Local);
147
148 let (_, tier) = cache.get_located(&key).unwrap().unwrap();
150 assert_eq!(tier, CacheTier::Memory);
151
152 let _ = fs::remove_dir_all(&dir);
153 }
154
155 #[test]
156 fn put_writes_to_all_tiers() {
157 let (cache, dir) = make_tiered();
158 let key = CacheKey::hash_data(b"test");
159 let value = Value::tensor(vec![1.0, 2.0], vec![2]);
160
161 cache.put(&key, &value).unwrap();
162
163 assert!(cache.tiers[0].1.exists(&key).unwrap()); assert!(cache.tiers[1].1.exists(&key).unwrap()); let _ = fs::remove_dir_all(&dir);
168 }
169
170 #[test]
171 fn get_from_memory_first() {
172 let (cache, dir) = make_tiered();
173 let key = CacheKey::hash_data(b"test");
174 let value = Value::tensor(vec![1.0], vec![1]);
175
176 cache.put(&key, &value).unwrap();
177 let result = cache.get(&key).unwrap().unwrap();
178 assert_eq!(result, value);
179
180 let _ = fs::remove_dir_all(&dir);
181 }
182
183 #[test]
184 fn promotes_from_local_to_memory() {
185 let dir = temp_dir();
186 let memory = Box::new(MemoryCache::default());
187 let local = Box::new(LocalCache::new(&dir).unwrap());
188
189 let key = CacheKey::hash_data(b"local_only");
191 let value = Value::tensor(vec![42.0], vec![1]);
192 local.put(&key, &value).unwrap();
193
194 let tiered = TieredCache::memory_and_local(memory, local);
195
196 assert!(!tiered.tiers[0].1.exists(&key).unwrap());
198
199 let result = tiered.get(&key).unwrap().unwrap();
201 assert_eq!(result, value);
202
203 assert!(tiered.tiers[0].1.exists(&key).unwrap());
205
206 let _ = fs::remove_dir_all(&dir);
207 }
208
209 #[test]
210 fn miss_returns_none() {
211 let (cache, dir) = make_tiered();
212 assert!(cache.get(&CacheKey::hash_data(b"nope")).unwrap().is_none());
213 let _ = fs::remove_dir_all(&dir);
214 }
215
216 #[test]
217 fn remove_from_all_tiers() {
218 let (cache, dir) = make_tiered();
219 let key = CacheKey::hash_data(b"test");
220 cache.put(&key, &Value::Empty).unwrap();
221 cache.remove(&key).unwrap();
222
223 assert!(!cache.tiers[0].1.exists(&key).unwrap());
224 assert!(!cache.tiers[1].1.exists(&key).unwrap());
225
226 let _ = fs::remove_dir_all(&dir);
227 }
228}