clawft_kernel/node_facts/
cache.rs1use dashmap::DashMap;
13
14use clawft_types::placement::{
15 Capability, FactsError, NodeFacts, NodeLoad, Provenance, TrustTier,
16};
17
18use crate::node_facts_advert::{
19 NodeFactsAdvertError, SignedFactsDelta, SignedNodeFacts, verify_facts_delta, verify_node_facts,
20};
21
22pub const MAX_CACHED_NODES: usize = 4096;
24pub const MAX_DISCOVERED: usize = 512;
26
27#[cfg(test)]
30pub(crate) mod pause {
31 use std::sync::Mutex;
32 use std::sync::atomic::{AtomicBool, Ordering};
33 pub(crate) static TARGET: Mutex<Option<(String, u64)>> = Mutex::new(None);
34 pub(crate) static PAUSED: AtomicBool = AtomicBool::new(false);
35 pub(crate) static RELEASE: AtomicBool = AtomicBool::new(false);
36 pub(crate) fn here(node_id: &str, seq: u64) {
37 let hit = TARGET.lock().unwrap().as_ref().is_some_and(|(n, s)| n == node_id && *s == seq);
38 if hit {
39 PAUSED.store(true, Ordering::SeqCst);
40 while !RELEASE.load(Ordering::SeqCst) {
41 std::thread::sleep(std::time::Duration::from_millis(1));
42 }
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum TierSource {
52 Mesh,
54 Operator,
56}
57
58#[derive(Debug, Clone, PartialEq)]
60pub struct CachedNodeFacts {
61 pub facts: NodeFacts,
63 pub signed: SignedNodeFacts,
65 pub trust_tier: TrustTier,
67 pub tier_source: TierSource,
69 pub origin: u64,
71 pub received_at: u64,
73 pub delta_seq: u64,
75 pub public_key: [u8; 32],
77}
78
79impl CachedNodeFacts {
80 pub fn node_id(&self) -> &str {
82 self.facts.node_id()
83 }
84 pub fn capabilities(&self) -> &[Capability] {
86 self.facts.capabilities()
87 }
88 pub fn trust_tier(&self) -> TrustTier {
90 self.trust_tier
91 }
92 pub fn load(&self) -> NodeLoad {
94 self.facts.load()
95 }
96 pub fn is_fresh(&self, now: u64) -> bool {
98 self.facts.is_fresh(now)
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum InsertOutcome {
105 Added,
107 Replaced,
109 Unchanged,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
115pub enum CacheError {
116 #[error(transparent)]
118 Verify(#[from] NodeFactsAdvertError),
119 #[error("stale facts for {node_id}: seq {got} <= held {held}")]
121 Stale {
122 node_id: String,
124 got: u64,
126 held: u64,
128 },
129 #[error("no fresh facts cached for {0}")]
131 UnknownNode(String),
132 #[error("delta key does not match the cached facts key")]
134 KeyMismatch,
135 #[error("node facts cache full")]
137 Full,
138}
139
140#[derive(Debug, Default)]
142pub struct NodeFactsCache {
143 entries: DashMap<String, CachedNodeFacts>,
144}
145
146impl NodeFactsCache {
147 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn insert(
154 &self,
155 signed: SignedNodeFacts,
156 trust_tier: TrustTier,
157 now: u64,
158 ) -> Result<InsertOutcome, CacheError> {
159 self.insert_capped(signed, trust_tier, now, None, 0)
160 }
161
162 pub fn insert_remote(
175 &self,
176 signed: SignedNodeFacts,
177 mesh_tier: TrustTier,
178 max_provenance: Provenance,
179 origin: u64,
180 now: u64,
181 ) -> Result<InsertOutcome, CacheError> {
182 self.insert_capped(signed, mesh_tier, now, Some(max_provenance), origin)
183 }
184
185 fn insert_capped(
186 &self,
187 signed: SignedNodeFacts,
188 trust_tier: TrustTier,
189 now: u64,
190 cap: Option<Provenance>,
191 origin: u64,
192 ) -> Result<InsertOutcome, CacheError> {
193 let remote = cap.is_some();
194 let mut facts = verify_node_facts(&signed, now)?;
195 if let Some(cap) = cap {
196 for c in &mut facts.capabilities {
197 c.provenance = c.provenance.min(cap);
198 }
199 }
200 let mut public_key = [0u8; 32];
201 public_key.copy_from_slice(&signed.public_key);
202 let node_id = facts.node_id.clone();
203 let mut entry = CachedNodeFacts {
204 facts,
205 signed,
206 trust_tier,
207 tier_source: if remote { TierSource::Mesh } else { TierSource::Operator },
208 origin,
209 received_at: now,
210 delta_seq: 0,
211 public_key,
212 };
213 if !self.entries.contains_key(&node_id) {
216 if remote
219 && entry.tier_source == TierSource::Mesh
220 && entry.trust_tier == TrustTier::Discovered
221 && self.count_discovered() >= MAX_DISCOVERED
222 && !self.evict_discovered_by_weight()
223 {
224 return Err(CacheError::Full);
225 }
226 if self.entries.len() >= MAX_CACHED_NODES {
227 self.evict_expired(now);
228 if self.entries.len() >= MAX_CACHED_NODES && !self.evict_discovered_by_weight() {
229 return Err(CacheError::Full);
230 }
231 }
232 }
233 use dashmap::mapref::entry::Entry;
237 match self.entries.entry(node_id.clone()) {
238 Entry::Vacant(v) => {
239 v.insert(entry);
240 Ok(InsertOutcome::Added)
241 }
242 Entry::Occupied(mut o) => {
243 if o.get().is_fresh(now) {
245 let held = o.get_mut();
246 if remote && held.tier_source == TierSource::Operator {
247 entry.trust_tier = held.trust_tier;
248 entry.tier_source = TierSource::Operator;
249 }
250 if held.signed.payload == entry.signed.payload {
251 if remote && held.tier_source == TierSource::Mesh {
252 held.trust_tier = trust_tier;
254 held.origin = origin;
255 }
256 return Ok(InsertOutcome::Unchanged);
257 }
258 if entry.facts.seq <= held.facts.seq {
259 return Err(CacheError::Stale {
260 node_id,
261 got: entry.facts.seq,
262 held: held.facts.seq,
263 });
264 }
265 }
266 #[cfg(test)]
267 pause::here(&node_id, entry.facts.seq);
268 o.insert(entry);
269 Ok(InsertOutcome::Replaced)
270 }
271 }
272 }
273
274 fn count_discovered(&self) -> usize {
275 self.entries
276 .iter()
277 .filter(|e| e.tier_source == TierSource::Mesh && e.trust_tier == TrustTier::Discovered)
278 .count()
279 }
280
281 fn evict_discovered_by_weight(&self) -> bool {
289 let mut by_origin: std::collections::HashMap<u64, (usize, u64, String)> = Default::default();
290 for e in self
291 .entries
292 .iter()
293 .filter(|e| e.tier_source == TierSource::Mesh && e.trust_tier == TrustTier::Discovered)
294 {
295 let group = if e.origin == 0 { u64::MAX - (by_origin.len() as u64) } else { e.origin };
297 let slot = by_origin
298 .entry(group)
299 .or_insert((0, u64::MAX, String::new()));
300 slot.0 += 1;
301 if (e.received_at, e.key().clone()) < (slot.1, slot.2.clone()) || slot.2.is_empty() {
302 slot.1 = e.received_at;
303 slot.2 = e.key().clone();
304 }
305 }
306 let victim = by_origin
307 .values()
308 .max_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)))
309 .map(|v| v.2.clone());
310 match victim {
311 Some(k) => self.entries.remove(&k).is_some(),
312 None => false,
313 }
314 }
315
316 pub fn discovered_from(&self, origin: u64, now: u64) -> usize {
318 if origin == 0 {
319 return 0;
320 }
321 self.entries
322 .iter()
323 .filter(|e| {
324 e.origin == origin
325 && e.tier_source == TierSource::Mesh
326 && e.trust_tier == TrustTier::Discovered
327 && e.is_fresh(now)
328 })
329 .count()
330 }
331
332 pub fn demote_mesh_tier(&self, node_id: &str) -> bool {
336 match self.entries.get_mut(node_id) {
337 Some(mut e)
338 if e.tier_source == TierSource::Mesh && e.trust_tier != TrustTier::Discovered =>
339 {
340 e.trust_tier = TrustTier::Discovered;
341 true
342 }
343 _ => false,
344 }
345 }
346
347 pub fn apply_delta(&self, signed: &SignedFactsDelta, now: u64) -> Result<(), CacheError> {
349 let (delta, pk) = verify_facts_delta(signed, now)?;
350 let mut entry = self
351 .entries
352 .get_mut(&delta.node_id)
353 .filter(|e| e.is_fresh(now))
354 .ok_or_else(|| CacheError::UnknownNode(delta.node_id.clone()))?;
355 if entry.public_key != pk {
356 return Err(CacheError::KeyMismatch);
357 }
358 if delta.seq <= entry.delta_seq {
359 return Err(NodeFactsAdvertError::Facts(FactsError::Delta(format!(
360 "delta seq {} <= applied {}",
361 delta.seq, entry.delta_seq
362 )))
363 .into());
364 }
365 entry
366 .facts
367 .apply_delta(&delta)
368 .map_err(NodeFactsAdvertError::Facts)?;
369 entry.delta_seq = delta.seq;
370 Ok(())
371 }
372
373 pub fn set_trust_tier(&self, node_id: &str, trust_tier: TrustTier) -> bool {
376 match self.entries.get_mut(node_id) {
377 Some(mut e) => {
378 e.trust_tier = trust_tier;
379 e.tier_source = TierSource::Operator;
380 true
381 }
382 None => false,
383 }
384 }
385
386 pub fn get(&self, node_id: &str, now: u64) -> Option<CachedNodeFacts> {
388 self.entries
389 .get(node_id)
390 .filter(|e| e.is_fresh(now))
391 .map(|e| e.clone())
392 }
393
394 pub fn list(&self, now: u64) -> Vec<CachedNodeFacts> {
396 let mut v: Vec<CachedNodeFacts> = self
397 .entries
398 .iter()
399 .filter(|e| e.is_fresh(now))
400 .map(|e| e.clone())
401 .collect();
402 v.sort_by(|a, b| a.node_id().cmp(b.node_id()));
403 v
404 }
405
406 pub fn evict_expired(&self, now: u64) -> usize {
408 let before = self.entries.len();
409 self.entries.retain(|_, e| e.is_fresh(now));
410 before - self.entries.len()
411 }
412
413 pub fn len(&self) -> usize {
415 self.entries.len()
416 }
417
418 pub fn is_empty(&self) -> bool {
420 self.entries.is_empty()
421 }
422}
423
424#[cfg(test)]
425#[path = "cache_tests.rs"]
426mod tests;