1use std::collections::{BTreeMap, HashSet};
16use std::sync::{Arc, Mutex};
17
18use clawft_types::placement::NodeFacts;
19
20use crate::mesh_artifact::ArtifactExchange;
21use crate::mesh_artifact_types::ArtifactId;
22use crate::mesh_swarm_governance::set_held_capabilities;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum ArtifactKind {
27 #[default]
29 Generic,
30 Model,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct CacheConfig {
37 pub max_bytes: u64,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
43pub enum CacheError {
44 #[error("artifact {0} is not held and verified")]
46 NotHeld(String),
47 #[error("artifact {id} ({size} bytes) exceeds the cache budget of {budget} bytes")]
49 TooLarge {
50 id: String,
52 size: u64,
54 budget: u64,
56 },
57 #[error("artifact {0} is pinned")]
59 Pinned(String),
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct CacheEntryInfo {
65 pub id: ArtifactId,
67 pub content_hash: [u8; 32],
69 pub size: u64,
71 pub pinned: bool,
73 pub kind: ArtifactKind,
75}
76
77struct Entry {
78 content_hash: [u8; 32],
79 size: u64,
80 pinned: bool,
81 kind: ArtifactKind,
82 last_used: u64,
83}
84
85#[derive(Default)]
86struct Inner {
87 entries: BTreeMap<ArtifactId, Entry>,
88 tick: u64,
89 pin_on_arrival: HashSet<[u8; 32]>,
91}
92
93impl Inner {
94 fn next_tick(&mut self) -> u64 {
95 self.tick += 1;
96 self.tick
97 }
98 fn used(&self) -> u64 {
99 self.entries.values().map(|e| e.size).sum()
100 }
101}
102
103fn package_hashes(pkg: &crate::mesh_artifact_pkg::ExchangedPackage) -> Vec<[u8; 32]> {
104 use crate::workload_pkg::codec::hex_decode_exact;
105 std::iter::once(pkg.manifest_hash.as_str())
106 .chain(pkg.verified.body.files().map(|f| f.blake3.as_str()))
107 .filter_map(hex_decode_exact::<32>)
108 .collect()
109}
110
111pub struct ArtifactCache {
113 ex: Arc<ArtifactExchange>,
114 cfg: CacheConfig,
115 inner: Mutex<Inner>,
116}
117
118impl ArtifactCache {
119 pub fn new(ex: Arc<ArtifactExchange>, cfg: CacheConfig) -> Arc<Self> {
123 let cache = Arc::new(Self {
124 ex: ex.clone(),
125 cfg,
126 inner: Mutex::new(Inner::default()),
127 });
128 ex.swarm.attach_cache(&cache);
129 for d in ex.descriptors.iter().map(|d| d.clone()).collect::<Vec<_>>() {
130 let _ = cache.admit(&d.id());
131 }
132 cache
133 }
134
135 fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
136 self.inner.lock().unwrap_or_else(|p| p.into_inner())
137 }
138
139 pub fn exchange(&self) -> &Arc<ArtifactExchange> {
141 &self.ex
142 }
143
144 pub fn admit(&self, id: &ArtifactId) -> Result<Vec<ArtifactId>, CacheError> {
148 let d = self
149 .ex
150 .descriptor(id)
151 .ok_or_else(|| CacheError::NotHeld(id.to_string()))?;
152 {
153 let mut g = self.lock();
154 let tick = g.next_tick();
155 let pinned = g.pin_on_arrival.contains(&d.content_hash);
156 g.entries
157 .entry(*id)
158 .and_modify(|e| e.last_used = tick)
159 .or_insert(Entry {
160 content_hash: d.content_hash,
161 size: d.total_size,
162 pinned,
163 kind: ArtifactKind::Generic,
164 last_used: tick,
165 });
166 }
167 let too_large = d.total_size > self.cfg.max_bytes && !self.is_pinned(id);
168 if too_large {
169 self.evict(id, "lru");
170 return Err(CacheError::TooLarge {
171 id: id.to_string(),
172 size: d.total_size,
173 budget: self.cfg.max_bytes,
174 });
175 }
176 Ok(self.enforce_keeping(Some(id)))
177 }
178
179 pub(crate) fn on_verified(&self, id: &ArtifactId) {
181 let _ = self.admit(id);
182 }
183
184 pub(crate) fn on_pending(&self) {
187 self.enforce();
188 }
189
190 pub(crate) fn on_forgotten(&self, id: &ArtifactId) {
192 self.lock().entries.remove(id);
193 }
194
195 pub fn touch(&self, id: &ArtifactId) {
197 let mut g = self.lock();
198 let tick = g.next_tick();
199 if let Some(e) = g.entries.get_mut(id) {
200 e.last_used = tick;
201 }
202 }
203
204 pub fn pin(&self, id: &ArtifactId) -> bool {
206 self.set_pinned(id, true)
207 }
208
209 pub fn unpin(&self, id: &ArtifactId) -> bool {
211 let known = self.set_pinned(id, false);
212 if known {
213 self.enforce();
214 }
215 known
216 }
217
218 fn set_pinned(&self, id: &ArtifactId, pinned: bool) -> bool {
219 let mut g = self.lock();
220 let tick = g.next_tick();
221 match g.entries.get_mut(id) {
222 Some(e) => {
223 e.pinned = pinned;
224 e.last_used = tick;
225 true
226 }
227 None => false,
228 }
229 }
230
231 pub fn pin_content(&self, content_hash: [u8; 32]) {
236 let mut g = self.lock();
237 g.pin_on_arrival.insert(content_hash);
238 for e in g.entries.values_mut().filter(|e| e.content_hash == content_hash) {
239 e.pinned = true;
240 }
241 }
242
243 pub fn unpin_content(&self, content_hash: &[u8; 32]) {
245 {
246 let mut g = self.lock();
247 g.pin_on_arrival.remove(content_hash);
248 for e in g.entries.values_mut().filter(|e| e.content_hash == *content_hash) {
249 e.pinned = false;
250 }
251 }
252 self.enforce();
253 }
254
255 pub fn pin_package(&self, pkg: &crate::mesh_artifact_pkg::ExchangedPackage) {
257 for h in package_hashes(pkg) {
258 self.pin_content(h);
259 }
260 }
261
262 pub fn unpin_package(&self, pkg: &crate::mesh_artifact_pkg::ExchangedPackage) {
264 for h in package_hashes(pkg) {
265 self.unpin_content(&h);
266 }
267 }
268
269 pub fn is_pinned(&self, id: &ArtifactId) -> bool {
271 self.lock().entries.get(id).is_some_and(|e| e.pinned)
272 }
273
274 pub fn set_kind(&self, id: &ArtifactId, kind: ArtifactKind) -> bool {
276 match self.lock().entries.get_mut(id) {
277 Some(e) => {
278 e.kind = kind;
279 true
280 }
281 None => false,
282 }
283 }
284
285 pub fn remove(&self, id: &ArtifactId) -> Result<(), CacheError> {
287 if self.is_pinned(id) {
288 return Err(CacheError::Pinned(id.to_string()));
289 }
290 if !self.lock().entries.contains_key(id) {
291 return Err(CacheError::NotHeld(id.to_string()));
292 }
293 self.evict(id, "manual");
294 Ok(())
295 }
296
297 pub fn enforce(&self) -> Vec<ArtifactId> {
300 self.enforce_keeping(None)
301 }
302
303 fn enforce_keeping(&self, keep: Option<&ArtifactId>) -> Vec<ArtifactId> {
306 let mut evicted = Vec::new();
307 loop {
308 let victim = {
309 let g = self.lock();
310 if g.used() + self.ex.pending_bytes() <= self.cfg.max_bytes {
311 break;
312 }
313 let oldest = |skip: Option<&ArtifactId>| {
314 g.entries
315 .iter()
316 .filter(|(id, e)| !e.pinned && Some(*id) != skip)
317 .min_by_key(|(_, e)| e.last_used)
318 .map(|(id, _)| *id)
319 };
320 oldest(keep).or_else(|| keep.and_then(|k| oldest(None).filter(|v| v == k)))
321 };
322 let Some(id) = victim else { break };
323 self.evict(&id, "lru");
324 evicted.push(id);
325 }
326 evicted
327 }
328
329 fn evict(&self, id: &ArtifactId, reason: &str) {
330 self.lock().entries.remove(id);
331 self.ex.forget(id, reason);
332 }
333
334 pub fn used_bytes(&self) -> u64 {
336 self.lock().used() + self.ex.pending_bytes()
337 }
338
339 pub fn over_budget(&self) -> u64 {
341 self.used_bytes().saturating_sub(self.cfg.max_bytes)
342 }
343
344 pub fn entries(&self) -> Vec<CacheEntryInfo> {
346 let g = self.lock();
347 let mut v: Vec<_> = g.entries.iter().collect();
348 v.sort_by_key(|(_, e)| e.last_used);
349 v.into_iter()
350 .map(|(id, e)| CacheEntryInfo {
351 id: *id,
352 content_hash: e.content_hash,
353 size: e.size,
354 pinned: e.pinned,
355 kind: e.kind,
356 })
357 .collect()
358 }
359
360 pub fn advertise(&self, facts: &mut NodeFacts) {
363 let shards: HashSet<[u8; 32]> = self
364 .lock()
365 .entries
366 .values()
367 .filter(|e| e.kind == ArtifactKind::Model)
368 .map(|e| e.content_hash)
369 .collect();
370 set_held_capabilities(facts, self.ex.held_capabilities(&shards));
371 }
372}
373
374#[cfg(test)]
375#[path = "mesh_swarm_cache_tests.rs"]
376mod tests;