use dashmap::DashMap;
use clawft_types::placement::{
Capability, FactsError, NodeFacts, NodeLoad, Provenance, TrustTier,
};
use crate::node_facts_advert::{
NodeFactsAdvertError, SignedFactsDelta, SignedNodeFacts, verify_facts_delta, verify_node_facts,
};
pub const MAX_CACHED_NODES: usize = 4096;
pub const MAX_DISCOVERED: usize = 512;
#[cfg(test)]
pub(crate) mod pause {
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
pub(crate) static TARGET: Mutex<Option<(String, u64)>> = Mutex::new(None);
pub(crate) static PAUSED: AtomicBool = AtomicBool::new(false);
pub(crate) static RELEASE: AtomicBool = AtomicBool::new(false);
pub(crate) fn here(node_id: &str, seq: u64) {
let hit = TARGET.lock().unwrap().as_ref().is_some_and(|(n, s)| n == node_id && *s == seq);
if hit {
PAUSED.store(true, Ordering::SeqCst);
while !RELEASE.load(Ordering::SeqCst) {
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TierSource {
Mesh,
Operator,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CachedNodeFacts {
pub facts: NodeFacts,
pub signed: SignedNodeFacts,
pub trust_tier: TrustTier,
pub tier_source: TierSource,
pub origin: u64,
pub received_at: u64,
pub delta_seq: u64,
pub public_key: [u8; 32],
}
impl CachedNodeFacts {
pub fn node_id(&self) -> &str {
self.facts.node_id()
}
pub fn capabilities(&self) -> &[Capability] {
self.facts.capabilities()
}
pub fn trust_tier(&self) -> TrustTier {
self.trust_tier
}
pub fn load(&self) -> NodeLoad {
self.facts.load()
}
pub fn is_fresh(&self, now: u64) -> bool {
self.facts.is_fresh(now)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InsertOutcome {
Added,
Replaced,
Unchanged,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CacheError {
#[error(transparent)]
Verify(#[from] NodeFactsAdvertError),
#[error("stale facts for {node_id}: seq {got} <= held {held}")]
Stale {
node_id: String,
got: u64,
held: u64,
},
#[error("no fresh facts cached for {0}")]
UnknownNode(String),
#[error("delta key does not match the cached facts key")]
KeyMismatch,
#[error("node facts cache full")]
Full,
}
#[derive(Debug, Default)]
pub struct NodeFactsCache {
entries: DashMap<String, CachedNodeFacts>,
}
impl NodeFactsCache {
pub fn new() -> Self {
Self::default()
}
pub fn insert(
&self,
signed: SignedNodeFacts,
trust_tier: TrustTier,
now: u64,
) -> Result<InsertOutcome, CacheError> {
self.insert_capped(signed, trust_tier, now, None, 0)
}
pub fn insert_remote(
&self,
signed: SignedNodeFacts,
mesh_tier: TrustTier,
max_provenance: Provenance,
origin: u64,
now: u64,
) -> Result<InsertOutcome, CacheError> {
self.insert_capped(signed, mesh_tier, now, Some(max_provenance), origin)
}
fn insert_capped(
&self,
signed: SignedNodeFacts,
trust_tier: TrustTier,
now: u64,
cap: Option<Provenance>,
origin: u64,
) -> Result<InsertOutcome, CacheError> {
let remote = cap.is_some();
let mut facts = verify_node_facts(&signed, now)?;
if let Some(cap) = cap {
for c in &mut facts.capabilities {
c.provenance = c.provenance.min(cap);
}
}
let mut public_key = [0u8; 32];
public_key.copy_from_slice(&signed.public_key);
let node_id = facts.node_id.clone();
let mut entry = CachedNodeFacts {
facts,
signed,
trust_tier,
tier_source: if remote { TierSource::Mesh } else { TierSource::Operator },
origin,
received_at: now,
delta_seq: 0,
public_key,
};
if !self.entries.contains_key(&node_id) {
if remote
&& entry.tier_source == TierSource::Mesh
&& entry.trust_tier == TrustTier::Discovered
&& self.count_discovered() >= MAX_DISCOVERED
&& !self.evict_discovered_by_weight()
{
return Err(CacheError::Full);
}
if self.entries.len() >= MAX_CACHED_NODES {
self.evict_expired(now);
if self.entries.len() >= MAX_CACHED_NODES && !self.evict_discovered_by_weight() {
return Err(CacheError::Full);
}
}
}
use dashmap::mapref::entry::Entry;
match self.entries.entry(node_id.clone()) {
Entry::Vacant(v) => {
v.insert(entry);
Ok(InsertOutcome::Added)
}
Entry::Occupied(mut o) => {
if o.get().is_fresh(now) {
let held = o.get_mut();
if remote && held.tier_source == TierSource::Operator {
entry.trust_tier = held.trust_tier;
entry.tier_source = TierSource::Operator;
}
if held.signed.payload == entry.signed.payload {
if remote && held.tier_source == TierSource::Mesh {
held.trust_tier = trust_tier;
held.origin = origin;
}
return Ok(InsertOutcome::Unchanged);
}
if entry.facts.seq <= held.facts.seq {
return Err(CacheError::Stale {
node_id,
got: entry.facts.seq,
held: held.facts.seq,
});
}
}
#[cfg(test)]
pause::here(&node_id, entry.facts.seq);
o.insert(entry);
Ok(InsertOutcome::Replaced)
}
}
}
fn count_discovered(&self) -> usize {
self.entries
.iter()
.filter(|e| e.tier_source == TierSource::Mesh && e.trust_tier == TrustTier::Discovered)
.count()
}
fn evict_discovered_by_weight(&self) -> bool {
let mut by_origin: std::collections::HashMap<u64, (usize, u64, String)> = Default::default();
for e in self
.entries
.iter()
.filter(|e| e.tier_source == TierSource::Mesh && e.trust_tier == TrustTier::Discovered)
{
let group = if e.origin == 0 { u64::MAX - (by_origin.len() as u64) } else { e.origin };
let slot = by_origin
.entry(group)
.or_insert((0, u64::MAX, String::new()));
slot.0 += 1;
if (e.received_at, e.key().clone()) < (slot.1, slot.2.clone()) || slot.2.is_empty() {
slot.1 = e.received_at;
slot.2 = e.key().clone();
}
}
let victim = by_origin
.values()
.max_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)))
.map(|v| v.2.clone());
match victim {
Some(k) => self.entries.remove(&k).is_some(),
None => false,
}
}
pub fn discovered_from(&self, origin: u64, now: u64) -> usize {
if origin == 0 {
return 0;
}
self.entries
.iter()
.filter(|e| {
e.origin == origin
&& e.tier_source == TierSource::Mesh
&& e.trust_tier == TrustTier::Discovered
&& e.is_fresh(now)
})
.count()
}
pub fn demote_mesh_tier(&self, node_id: &str) -> bool {
match self.entries.get_mut(node_id) {
Some(mut e)
if e.tier_source == TierSource::Mesh && e.trust_tier != TrustTier::Discovered =>
{
e.trust_tier = TrustTier::Discovered;
true
}
_ => false,
}
}
pub fn apply_delta(&self, signed: &SignedFactsDelta, now: u64) -> Result<(), CacheError> {
let (delta, pk) = verify_facts_delta(signed, now)?;
let mut entry = self
.entries
.get_mut(&delta.node_id)
.filter(|e| e.is_fresh(now))
.ok_or_else(|| CacheError::UnknownNode(delta.node_id.clone()))?;
if entry.public_key != pk {
return Err(CacheError::KeyMismatch);
}
if delta.seq <= entry.delta_seq {
return Err(NodeFactsAdvertError::Facts(FactsError::Delta(format!(
"delta seq {} <= applied {}",
delta.seq, entry.delta_seq
)))
.into());
}
entry
.facts
.apply_delta(&delta)
.map_err(NodeFactsAdvertError::Facts)?;
entry.delta_seq = delta.seq;
Ok(())
}
pub fn set_trust_tier(&self, node_id: &str, trust_tier: TrustTier) -> bool {
match self.entries.get_mut(node_id) {
Some(mut e) => {
e.trust_tier = trust_tier;
e.tier_source = TierSource::Operator;
true
}
None => false,
}
}
pub fn get(&self, node_id: &str, now: u64) -> Option<CachedNodeFacts> {
self.entries
.get(node_id)
.filter(|e| e.is_fresh(now))
.map(|e| e.clone())
}
pub fn list(&self, now: u64) -> Vec<CachedNodeFacts> {
let mut v: Vec<CachedNodeFacts> = self
.entries
.iter()
.filter(|e| e.is_fresh(now))
.map(|e| e.clone())
.collect();
v.sort_by(|a, b| a.node_id().cmp(b.node_id()));
v
}
pub fn evict_expired(&self, now: u64) -> usize {
let before = self.entries.len();
self.entries.retain(|_, e| e.is_fresh(now));
before - self.entries.len()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
#[path = "cache_tests.rs"]
mod tests;