use std::borrow::Borrow;
use std::hash::Hash;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use lru::LruCache;
pub(crate) type InterestPredicate<K> = Arc<dyn Fn(&K) -> bool + Send + Sync>;
pub(crate) const INTEREST_TIERED_ENV: &str = "FREENET_MODULE_CACHE_INTEREST_TIERED";
fn parse_interest_tiered_flag(value: Option<&str>) -> bool {
!matches!(
value.map(|v| v.trim().to_ascii_lowercase()).as_deref(),
Some("0" | "false" | "no" | "off" | "")
)
}
pub(crate) fn interest_tiered_enabled() -> bool {
parse_interest_tiered_flag(std::env::var(INTEREST_TIERED_ENV).ok().as_deref())
}
const EVICTION_WARN_INTERVAL: Duration = Duration::from_secs(300);
const EVICTION_WARN_THRESHOLD: u64 = 16;
const INTEREST_SHADOW_REFRESH_INTERVAL: Duration = Duration::from_secs(10);
pub(crate) struct ModuleCache<K: Hash + Eq, V> {
inner: LruCache<K, (V, usize)>,
total_bytes: usize,
budget_bytes: usize,
label: &'static str,
evictions_in_window: u64,
window_started_at: Option<Instant>,
metrics: Option<Arc<ModuleCacheMetrics>>,
interest: Option<InterestPredicate<K>>,
interest_tiered_active: bool,
last_interest_shadow_refresh: Option<Instant>,
}
impl<K: Hash + Eq + Clone, V> ModuleCache<K, V> {
pub(crate) fn new(budget_bytes: usize) -> Self {
Self::with_label(budget_bytes, "module", None)
}
pub(crate) fn with_label(
budget_bytes: usize,
label: &'static str,
metrics: Option<Arc<ModuleCacheMetrics>>,
) -> Self {
Self::with_label_and_interest(budget_bytes, label, metrics, None)
}
pub(crate) fn with_label_and_interest(
budget_bytes: usize,
label: &'static str,
metrics: Option<Arc<ModuleCacheMetrics>>,
interest: Option<InterestPredicate<K>>,
) -> Self {
let budget_bytes = budget_bytes.max(1);
let interest_tiered_active = interest.is_some() && interest_tiered_enabled();
let cache = Self {
inner: LruCache::unbounded(),
total_bytes: 0,
budget_bytes,
label,
evictions_in_window: 0,
window_started_at: None,
metrics,
interest,
interest_tiered_active,
last_interest_shadow_refresh: None,
};
cache.record_occupancy(0, 0, budget_bytes);
cache
}
#[cfg(test)]
pub(crate) fn with_interest_for_test(
budget_bytes: usize,
label: &'static str,
metrics: Option<Arc<ModuleCacheMetrics>>,
interest: InterestPredicate<K>,
tiered_active: bool,
) -> Self {
let mut cache = Self::with_label_and_interest(budget_bytes, label, metrics, Some(interest));
cache.interest_tiered_active = tiered_active;
cache
}
pub(crate) fn get<Q>(&mut self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let hit = self.inner.get(key).is_some();
self.maybe_refresh_interest_shadow();
if hit {
self.inner.get(key).map(|(v, _)| v)
} else {
None
}
}
fn maybe_refresh_interest_shadow(&mut self) {
if self.interest.is_none() || self.metrics.is_none() {
return;
}
let now = Instant::now();
let due = match self.last_interest_shadow_refresh {
Some(prev) => now.duration_since(prev) >= INTEREST_SHADOW_REFRESH_INTERVAL,
None => true,
};
if due {
self.last_interest_shadow_refresh = Some(now);
self.record_interest_shadow();
}
}
pub(crate) fn insert(&mut self, key: K, value: V, size_bytes: usize) {
if let Some((_, old_size)) = self.inner.put(key, (value, size_bytes)) {
self.total_bytes = self.total_bytes.saturating_sub(old_size);
}
self.total_bytes = self.total_bytes.saturating_add(size_bytes);
self.evict_to_budget();
self.record_occupancy(self.inner.len(), self.total_bytes, self.budget_bytes);
self.maybe_refresh_interest_shadow();
}
fn record_occupancy(&self, entries: usize, total_bytes: usize, budget_bytes: usize) {
if let Some(metrics) = &self.metrics {
metrics.record_occupancy(self.label, entries, total_bytes, budget_bytes);
}
}
fn add_evictions(&self, count: u64) {
if let Some(metrics) = &self.metrics {
metrics.add_evictions(self.label, count);
}
}
fn is_interested(&self, key: &K) -> bool {
self.interest
.as_ref()
.map(|pred| pred(key))
.unwrap_or(false)
}
fn lru_cold_key(&self) -> Option<K> {
self.inner
.iter()
.rev()
.map(|(k, _)| k)
.find(|k| !self.is_interested(k))
.cloned()
}
fn evict_to_budget(&mut self) {
let mut evicted_this_insert: u64 = 0;
while self.total_bytes > self.budget_bytes && self.inner.len() > 1 {
let evicted_size = if self.interest_tiered_active {
match self.lru_cold_key() {
Some(cold_key) => self.inner.pop(&cold_key).map(|(_, size)| size),
None => self.inner.pop_lru().map(|(_, (_, size))| size),
}
} else {
self.inner.pop_lru().map(|(_, (_, size))| size)
};
match evicted_size {
Some(size) => {
self.total_bytes = self.total_bytes.saturating_sub(size);
evicted_this_insert += 1;
}
None => break,
}
}
if evicted_this_insert > 0 {
self.note_evictions(evicted_this_insert);
self.add_evictions(evicted_this_insert);
}
}
fn record_interest_shadow(&self) {
if let Some(true) = self.recompute_interest_split() {
if let Some(metrics) = &self.metrics {
metrics.add_would_reclassify(self.label, 1);
}
}
}
fn recompute_interest_split(&self) -> Option<bool> {
let metrics = self.metrics.as_ref()?;
self.interest.as_ref()?;
let mut cold_evictable_bytes: u64 = 0;
let mut interested_bytes: u64 = 0;
let mut any_cold = false;
let mut lru_is_interested = false;
for (k, (_, size)) in self.inner.iter() {
let interested = self.is_interested(k);
if interested {
interested_bytes = interested_bytes.saturating_add(*size as u64);
} else {
cold_evictable_bytes = cold_evictable_bytes.saturating_add(*size as u64);
any_cold = true;
}
lru_is_interested = interested;
}
metrics.record_interest_shadow(self.label, cold_evictable_bytes, interested_bytes);
Some(lru_is_interested && any_cold)
}
fn note_evictions(&mut self, count: u64) {
let now = Instant::now();
let window_start = *self.window_started_at.get_or_insert(now);
self.evictions_in_window = self.evictions_in_window.saturating_add(count);
if now.duration_since(window_start) >= EVICTION_WARN_INTERVAL {
if self.evictions_in_window >= EVICTION_WARN_THRESHOLD {
tracing::warn!(
cache = self.label,
evictions = self.evictions_in_window,
window_secs = EVICTION_WARN_INTERVAL.as_secs(),
budget_bytes = self.budget_bytes,
total_bytes = self.total_bytes,
entries = self.inner.len(),
"compiled-WASM {} cache is persistently evicting at its byte budget — \
the hosted working set exceeds the cache, so modules are being \
recompiled on access. Raise FREENET_MODULE_CACHE_BUDGET_BYTES / \
module-cache-budget-bytes if this node has spare RAM, or accept the \
recompile cost if it is memory-bound.",
self.label,
);
}
self.evictions_in_window = 0;
self.window_started_at = Some(now);
}
}
pub(crate) fn remove<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let (value, size) = self.inner.pop(key)?;
self.total_bytes = self.total_bytes.saturating_sub(size);
self.record_occupancy(self.inner.len(), self.total_bytes, self.budget_bytes);
self.maybe_refresh_interest_shadow();
Some(value)
}
#[cfg(test)]
pub(crate) fn total_bytes(&self) -> usize {
self.total_bytes
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.inner.len()
}
#[cfg(test)]
pub(crate) fn budget_bytes(&self) -> usize {
self.budget_bytes
}
#[cfg(test)]
pub(crate) fn interest_tiered_active(&self) -> bool {
self.interest_tiered_active
}
pub(crate) fn force_refresh_interest_shadow(&mut self) {
self.last_interest_shadow_refresh = Some(Instant::now());
self.record_interest_shadow();
}
pub(crate) fn force_refresh_interest_gauges_only(&self) {
self.recompute_interest_split();
}
#[cfg(test)]
pub(crate) fn arm_interest_shadow_refresh(&mut self) {
self.last_interest_shadow_refresh = None;
}
}
#[derive(Default)]
struct CacheGauges {
entries: AtomicU64,
total_bytes: AtomicU64,
budget_bytes: AtomicU64,
evictions_total: AtomicU64,
cold_evictable_bytes: AtomicU64,
interested_bytes: AtomicU64,
evictions_would_reclassify_total: AtomicU64,
}
pub(crate) struct ModuleCacheMetrics {
contract: CacheGauges,
delegate: CacheGauges,
migration_admission_recovered_total: AtomicU64,
interest_shadow_refresher: std::sync::OnceLock<Arc<dyn Fn() + Send + Sync>>,
interest_gauges_refresher: std::sync::OnceLock<Arc<dyn Fn() + Send + Sync>>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ModuleCacheMetricsSnapshot {
pub contract_entries: u64,
pub contract_total_bytes: u64,
pub contract_budget_bytes: u64,
pub contract_evictions_total: u64,
pub delegate_entries: u64,
pub delegate_total_bytes: u64,
pub delegate_budget_bytes: u64,
pub delegate_evictions_total: u64,
pub contract_cold_evictable_bytes: u64,
pub contract_interested_bytes: u64,
pub contract_evictions_would_reclassify_total: u64,
pub migration_admission_recovered_total: u64,
}
impl ModuleCacheMetrics {
pub(crate) fn new() -> Self {
Self {
contract: CacheGauges::default(),
delegate: CacheGauges::default(),
migration_admission_recovered_total: AtomicU64::new(0),
interest_shadow_refresher: std::sync::OnceLock::new(),
interest_gauges_refresher: std::sync::OnceLock::new(),
}
}
pub(crate) fn set_interest_shadow_refresher(&self, refresher: Arc<dyn Fn() + Send + Sync>) {
if self.interest_shadow_refresher.set(refresher).is_err() {
tracing::debug!(
"module-cache interest-shadow refresher already set; ignoring re-registration"
);
}
}
pub(crate) fn refresh_interest_shadow_now(&self) {
if let Some(refresher) = self.interest_shadow_refresher.get() {
refresher();
}
}
pub(crate) fn set_interest_gauges_refresher(&self, refresher: Arc<dyn Fn() + Send + Sync>) {
if self.interest_gauges_refresher.set(refresher).is_err() {
tracing::debug!(
"module-cache interest-gauges refresher already set; ignoring re-registration"
);
}
}
pub(crate) fn refresh_interest_gauges_now(&self) {
if let Some(refresher) = self.interest_gauges_refresher.get() {
refresher();
}
}
fn gauges_for(&self, label: &str) -> Option<&CacheGauges> {
match label {
"contract" => Some(&self.contract),
"delegate" => Some(&self.delegate),
_ => None,
}
}
fn record_occupancy(
&self,
label: &str,
entries: usize,
total_bytes: usize,
budget_bytes: usize,
) {
if let Some(g) = self.gauges_for(label) {
g.entries.store(entries as u64, Ordering::Relaxed);
g.total_bytes.store(total_bytes as u64, Ordering::Relaxed);
g.budget_bytes.store(budget_bytes as u64, Ordering::Relaxed);
}
}
fn add_evictions(&self, label: &str, count: u64) {
if let Some(g) = self.gauges_for(label) {
g.evictions_total.fetch_add(count, Ordering::Relaxed);
}
}
fn record_interest_shadow(
&self,
label: &str,
cold_evictable_bytes: u64,
interested_bytes: u64,
) {
if let Some(g) = self.gauges_for(label) {
g.cold_evictable_bytes
.store(cold_evictable_bytes, Ordering::Relaxed);
g.interested_bytes
.store(interested_bytes, Ordering::Relaxed);
}
}
fn add_would_reclassify(&self, label: &str, count: u64) {
if let Some(g) = self.gauges_for(label) {
g.evictions_would_reclassify_total
.fetch_add(count, Ordering::Relaxed);
}
}
pub(crate) fn record_migration_admission_recovered(&self) {
self.migration_admission_recovered_total
.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn snapshot(&self) -> ModuleCacheMetricsSnapshot {
let load = |g: &CacheGauges| {
(
g.entries.load(Ordering::Relaxed),
g.total_bytes.load(Ordering::Relaxed),
g.budget_bytes.load(Ordering::Relaxed),
g.evictions_total.load(Ordering::Relaxed),
)
};
let (ce, ctb, cbb, cev) = load(&self.contract);
let (de, dtb, dbb, dev) = load(&self.delegate);
ModuleCacheMetricsSnapshot {
contract_entries: ce,
contract_total_bytes: ctb,
contract_budget_bytes: cbb,
contract_evictions_total: cev,
delegate_entries: de,
delegate_total_bytes: dtb,
delegate_budget_bytes: dbb,
delegate_evictions_total: dev,
contract_cold_evictable_bytes: self
.contract
.cold_evictable_bytes
.load(Ordering::Relaxed),
contract_interested_bytes: self.contract.interested_bytes.load(Ordering::Relaxed),
contract_evictions_would_reclassify_total: self
.contract
.evictions_would_reclassify_total
.load(Ordering::Relaxed),
migration_admission_recovered_total: self
.migration_admission_recovered_total
.load(Ordering::Relaxed),
}
}
}
#[cfg(test)]
impl ModuleCacheMetrics {
pub(crate) fn with_contract_gauges_for_test(
total_bytes: u64,
budget_bytes: u64,
interested_bytes: u64,
) -> Self {
let m = Self::new();
m.record_occupancy("contract", 0, total_bytes as usize, budget_bytes as usize);
m.record_interest_shadow(
"contract",
total_bytes.saturating_sub(interested_bytes),
interested_bytes,
);
m
}
}
pub(crate) fn contract_cache_occupancy_pct(metrics: &ModuleCacheMetrics) -> Option<u64> {
let snapshot = metrics.snapshot();
occupancy_pct(
snapshot.contract_total_bytes,
snapshot.contract_budget_bytes,
)
}
pub(crate) fn contract_cache_interested_occupancy_pct(metrics: &ModuleCacheMetrics) -> Option<u64> {
let snapshot = metrics.snapshot();
occupancy_pct(
snapshot.contract_interested_bytes,
snapshot.contract_budget_bytes,
)
}
fn occupancy_pct(total_bytes: u64, budget_bytes: u64) -> Option<u64> {
if budget_bytes == 0 {
return None;
}
Some(total_bytes.saturating_mul(100) / budget_bytes)
}
fn migration_admission_recovered(
total_bytes: u64,
interested_bytes: u64,
budget_bytes: u64,
ceiling_pct: u64,
) -> bool {
let Some(occupancy) = occupancy_pct(total_bytes, budget_bytes) else {
return false;
};
let old_raw_gate_refused = occupancy >= ceiling_pct;
let new_interested_gate_admits = match occupancy_pct(interested_bytes, budget_bytes) {
Some(interested_pct) => interested_pct < ceiling_pct,
None => false,
};
old_raw_gate_refused && new_interested_gate_admits
}
pub(crate) fn migration_admission_recovered_now(
metrics: &ModuleCacheMetrics,
ceiling_pct: u64,
) -> bool {
let s = metrics.snapshot();
migration_admission_recovered(
s.contract_total_bytes,
s.contract_interested_bytes,
s.contract_budget_bytes,
ceiling_pct,
)
}
pub const MIN_DEFAULT_MODULE_CACHE_BUDGET_BYTES: usize = 64 * 1024 * 1024;
pub const MAX_DEFAULT_MODULE_CACHE_BUDGET_BYTES: usize = 1536 * 1024 * 1024;
const DEFAULT_MODULE_CACHE_RAM_DIVISOR: usize = 8;
pub const DELEGATE_MODULE_CACHE_BUDGET_DIVISOR: usize = 4;
const FALLBACK_TOTAL_RAM_BYTES: usize = 1024 * 1024 * 1024;
pub fn default_module_cache_budget_bytes() -> usize {
let total_ram = read_total_ram_bytes().unwrap_or(FALLBACK_TOTAL_RAM_BYTES);
budget_for_ram(total_ram)
}
fn budget_for_ram(total_ram: usize) -> usize {
(total_ram / DEFAULT_MODULE_CACHE_RAM_DIVISOR).clamp(
MIN_DEFAULT_MODULE_CACHE_BUDGET_BYTES,
MAX_DEFAULT_MODULE_CACHE_BUDGET_BYTES,
)
}
pub(crate) fn read_total_ram_bytes() -> Option<usize> {
#[cfg(target_os = "linux")]
{
let phys = read_proc_meminfo_total_bytes();
match (phys, read_cgroup_memory_limit_bytes()) {
(Some(p), Some(c)) => Some(p.min(c)),
(Some(p), None) => Some(p),
(None, Some(c)) => Some(c),
(None, None) => None,
}
}
#[cfg(all(unix, not(target_os = "linux")))]
{
let pages = unsafe { libc::sysconf(libc::_SC_PHYS_PAGES) };
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if pages > 0 && page_size > 0 {
(pages as usize).checked_mul(page_size as usize)
} else {
None
}
}
#[cfg(not(unix))]
{
None
}
}
#[cfg(target_os = "linux")]
fn read_proc_meminfo_total_bytes() -> Option<usize> {
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
parse_meminfo_total_bytes(&meminfo)
}
#[cfg(target_os = "linux")]
fn parse_meminfo_total_bytes(meminfo: &str) -> Option<usize> {
for line in meminfo.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
let kib: usize = rest.split_whitespace().next()?.parse().ok()?;
return kib.checked_mul(1024);
}
}
None
}
#[cfg(target_os = "linux")]
fn read_cgroup_memory_limit_bytes() -> Option<usize> {
let (v2_rel, v1_rel) = cgroup_relative_paths();
let v2 = min_cgroup_limit_over_ancestors("/sys/fs/cgroup", "memory.max", &v2_rel);
let v1 =
min_cgroup_limit_over_ancestors("/sys/fs/cgroup/memory", "memory.limit_in_bytes", &v1_rel);
match (v2, v1) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
#[cfg(target_os = "linux")]
fn min_cgroup_limit_over_ancestors(mount: &str, file: &str, rel: &str) -> Option<usize> {
let mut best: Option<usize> = None;
for path in cgroup_ancestor_limit_paths(mount, file, rel) {
if let Ok(s) = std::fs::read_to_string(&path) {
if let Some(v) = parse_cgroup_limit(&s) {
best = Some(best.map_or(v, |b: usize| b.min(v)));
}
}
}
best
}
#[cfg(target_os = "linux")]
fn cgroup_ancestor_limit_paths(mount: &str, file: &str, rel: &str) -> Vec<String> {
let mut paths = Vec::new();
let mut current = rel.trim_end_matches('/').to_string();
loop {
paths.push(format!("{mount}{current}/{file}"));
if current.is_empty() {
break; }
match current.rfind('/') {
Some(idx) => current.truncate(idx),
None => current.clear(),
}
}
paths
}
#[cfg(target_os = "linux")]
fn cgroup_relative_paths() -> (String, String) {
match std::fs::read_to_string("/proc/self/cgroup") {
Ok(s) => parse_proc_self_cgroup(&s),
Err(_) => ("/".to_string(), "/".to_string()),
}
}
#[cfg(target_os = "linux")]
fn parse_proc_self_cgroup(contents: &str) -> (String, String) {
let mut v2 = "/".to_string();
let mut v1 = "/".to_string();
for line in contents.lines() {
let mut parts = line.splitn(3, ':');
let (Some(_id), Some(controllers), Some(path)) = (parts.next(), parts.next(), parts.next())
else {
continue;
};
if controllers.is_empty() {
v2 = path.to_string();
} else if controllers.split(',').any(|c| c == "memory") {
v1 = path.to_string();
}
}
(v2, v1)
}
#[cfg(target_os = "linux")]
const CGROUP_UNLIMITED_THRESHOLD_BYTES: usize = 1 << 60;
#[cfg(target_os = "linux")]
fn parse_cgroup_limit(contents: &str) -> Option<usize> {
let trimmed = contents.trim();
if trimmed == "max" {
return None;
}
let bytes: usize = trimmed.parse().ok()?;
if bytes == 0 || bytes >= CGROUP_UNLIMITED_THRESHOLD_BYTES {
return None;
}
Some(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn many_small_entries_fit_when_total_within_budget() {
let mut cache: ModuleCache<u64, ()> = ModuleCache::new(5_000_000);
for k in 0..5000u64 {
cache.insert(k, (), 1000);
}
assert_eq!(
cache.len(),
5000,
"5000 small entries (far above the old 1024 count cap) should all fit"
);
assert_eq!(cache.total_bytes(), 5_000_000);
assert!(cache.total_bytes() <= cache.budget_bytes());
for k in 0..5000u64 {
assert!(cache.get(&k).is_some(), "key {k} should still be cached");
}
}
#[test]
fn large_entries_evicted_below_old_count_cap() {
let budget = 10 * 1024 * 1024;
let entry = 4 * 1024 * 1024;
let mut cache: ModuleCache<u64, ()> = ModuleCache::new(budget);
cache.insert(0, (), entry);
cache.insert(1, (), entry);
assert_eq!(cache.len(), 2);
assert_eq!(cache.total_bytes(), 2 * entry);
cache.insert(2, (), entry);
assert!(
cache.total_bytes() <= budget,
"total_bytes {} must stay within budget {}",
cache.total_bytes(),
budget
);
assert_eq!(cache.len(), 2, "evicted below the old 1024 count cap");
assert!(cache.get(&0).is_none(), "LRU entry 0 should be evicted");
assert!(cache.get(&1).is_some(), "entry 1 should remain");
assert!(cache.get(&2).is_some(), "entry 2 should remain");
}
#[test]
fn get_refreshes_lru_recency() {
let budget = 10 * 1024 * 1024;
let entry = 4 * 1024 * 1024;
let mut cache: ModuleCache<u64, ()> = ModuleCache::new(budget);
cache.insert(0, (), entry);
cache.insert(1, (), entry);
assert!(cache.get(&0).is_some());
cache.insert(2, (), entry);
assert!(
cache.get(&1).is_none(),
"untouched entry 1 should be evicted"
);
assert!(
cache.get(&0).is_some(),
"recently-touched entry 0 should remain"
);
assert!(cache.get(&2).is_some());
}
#[test]
fn replacing_key_updates_total_bytes() {
let mut cache: ModuleCache<u64, ()> = ModuleCache::new(1_000_000);
cache.insert(0, (), 100);
assert_eq!(cache.total_bytes(), 100);
cache.insert(0, (), 250);
assert_eq!(cache.len(), 1, "replacement, not a second entry");
assert_eq!(
cache.total_bytes(),
250,
"old size removed, new size added — no double count"
);
}
#[test]
fn oversized_single_value_is_retained() {
let mut cache: ModuleCache<u64, ()> = ModuleCache::new(1000);
cache.insert(0, (), 5000);
assert_eq!(cache.len(), 1, "oversized value still stored");
assert_eq!(cache.total_bytes(), 5000);
assert!(cache.get(&0).is_some());
cache.insert(1, (), 200);
assert_eq!(cache.len(), 1);
assert!(cache.get(&0).is_none(), "oversized LRU evicted");
assert!(cache.get(&1).is_some());
assert_eq!(cache.total_bytes(), 200);
}
#[test]
fn remove_decrements_total_bytes() {
let mut cache: ModuleCache<u64, &'static str> = ModuleCache::new(1_000_000);
cache.insert(0, "a", 100);
cache.insert(1, "b", 250);
assert_eq!(cache.total_bytes(), 350);
assert_eq!(cache.len(), 2);
assert_eq!(cache.remove(&0), Some("a"));
assert_eq!(
cache.total_bytes(),
250,
"removed entry's 100 bytes subtracted"
);
assert_eq!(cache.len(), 1);
assert_eq!(cache.remove(&42), None);
assert_eq!(cache.total_bytes(), 250);
assert_eq!(cache.len(), 1);
assert_eq!(cache.remove(&1), Some("b"));
assert_eq!(cache.total_bytes(), 0);
assert_eq!(cache.len(), 0);
}
#[test]
fn default_budget_is_within_clamp() {
let b = default_module_cache_budget_bytes();
assert!(
(MIN_DEFAULT_MODULE_CACHE_BUDGET_BYTES..=MAX_DEFAULT_MODULE_CACHE_BUDGET_BYTES)
.contains(&b),
"default budget {b} must be within [{MIN_DEFAULT_MODULE_CACHE_BUDGET_BYTES}, \
{MAX_DEFAULT_MODULE_CACHE_BUDGET_BYTES}]"
);
}
#[test]
fn combined_default_ceiling_is_safe_on_small_box() {
for total_ram_gib_eighths in 1..=96u64 {
let total_ram = (total_ram_gib_eighths as usize) * (128 * 1024 * 1024);
let contract = budget_for_ram(total_ram);
let delegate = contract / DELEGATE_MODULE_CACHE_BUDGET_DIVISOR;
let combined = contract + delegate;
assert!(
combined <= total_ram,
"combined default {combined} must never exceed total RAM \
({total_ram}) — a MIN raise above a small box's RAM would OOM it"
);
if total_ram
>= 4 * (MIN_DEFAULT_MODULE_CACHE_BUDGET_BYTES
+ MIN_DEFAULT_MODULE_CACHE_BUDGET_BYTES / DELEGATE_MODULE_CACHE_BUDGET_DIVISOR)
{
assert!(
combined <= total_ram / 4,
"combined default {combined} must stay within 1/4 of RAM \
({total_ram}); the divisor, not the MAX clamp, protects small boxes"
);
}
}
}
#[ignore = "superseded by #4481: MAX raised 384 MiB -> 1.5 GiB; see replacement tests"]
#[test]
fn combined_default_ceiling_is_safe() {
let contract = MAX_DEFAULT_MODULE_CACHE_BUDGET_BYTES;
let delegate = contract / DELEGATE_MODULE_CACHE_BUDGET_DIVISOR;
let combined = contract + delegate;
assert!(
combined < 512 * 1024 * 1024,
"combined default ceiling {combined} must stay under 512 MiB so the \
#4441 OOM fix doesn't itself OOM a small box (was 768 MiB pre-fix)"
);
}
#[test]
fn max_clamp_combined_ceiling_is_safe_at_binding_host() {
let contract = MAX_DEFAULT_MODULE_CACHE_BUDGET_BYTES;
let delegate = contract / DELEGATE_MODULE_CACHE_BUDGET_DIVISOR;
let combined = contract + delegate;
let binding_host_ram =
MAX_DEFAULT_MODULE_CACHE_BUDGET_BYTES * DEFAULT_MODULE_CACHE_RAM_DIVISOR;
assert!(
combined <= binding_host_ram / 4,
"combined default at the MAX clamp ({combined}) must stay within 1/4 \
of the smallest host where MAX binds ({binding_host_ram}); a MAX \
raise that breaks this would OOM a just-large-enough box"
);
}
#[test]
fn large_gateway_default_exceeds_old_clamp() {
const OLD_MAX: usize = 384 * 1024 * 1024;
const MEASURED_MODULE_SIZE: usize = 1536 * 1024;
let nova_ram = 125 * 1024 * 1024 * 1024usize;
let budget = budget_for_ram(nova_ram);
assert_eq!(
budget, MAX_DEFAULT_MODULE_CACHE_BUDGET_BYTES,
"a >12 GiB host lands on the MAX clamp"
);
assert!(
budget > OLD_MAX,
"the new default ceiling {budget} must exceed the pre-#4441 384 MiB \
clamp that caused gateway module-cache thrash"
);
let modules_held = budget / MEASURED_MODULE_SIZE;
assert!(
modules_held >= 900,
"the default ceiling must hold a realistic gateway working set \
(~1000 modules at ~1.5 MiB each); holds {modules_held}"
);
}
#[cfg(target_os = "linux")]
#[test]
fn parse_cgroup_limit_distinguishes_real_limits_from_sentinels() {
assert_eq!(
parse_cgroup_limit("2147483648\n"),
Some(2 * 1024 * 1024 * 1024)
);
assert_eq!(parse_cgroup_limit(" 536870912 "), Some(512 * 1024 * 1024));
assert_eq!(parse_cgroup_limit("max\n"), None);
assert_eq!(parse_cgroup_limit("9223372036854771712"), None);
assert_eq!(parse_cgroup_limit(&format!("{}", 1usize << 61)), None);
assert_eq!(parse_cgroup_limit("0"), None);
assert_eq!(parse_cgroup_limit(""), None);
assert_eq!(parse_cgroup_limit("not-a-number"), None);
}
#[cfg(target_os = "linux")]
#[test]
fn parse_meminfo_total_reads_kib_as_bytes() {
let sample = "MemFree: 100 kB\nMemTotal: 16331752 kB\nBuffers: 1 kB\n";
assert_eq!(parse_meminfo_total_bytes(sample), Some(16331752 * 1024));
assert_eq!(parse_meminfo_total_bytes("SwapTotal: 0 kB\n"), None);
}
#[cfg(target_os = "linux")]
#[test]
fn parse_proc_self_cgroup_resolves_own_path() {
let v2_only = "0::/system.slice/freenet-gateway.service\n";
assert_eq!(
parse_proc_self_cgroup(v2_only),
(
"/system.slice/freenet-gateway.service".to_string(),
"/".to_string()
)
);
let hybrid = "12:cpu,memory:/docker/abc123\n0::/docker/abc123\n";
assert_eq!(
parse_proc_self_cgroup(hybrid),
("/docker/abc123".to_string(), "/docker/abc123".to_string())
);
let v1 = "8:memory:/kubepods/pod1\n9:cpuset:/elsewhere\n";
assert_eq!(
parse_proc_self_cgroup(v1),
("/".to_string(), "/kubepods/pod1".to_string())
);
assert_eq!(
parse_proc_self_cgroup("0::/\n"),
("/".to_string(), "/".to_string())
);
assert_eq!(
parse_proc_self_cgroup(""),
("/".to_string(), "/".to_string())
);
let weird = "0::/odd:name/x\n";
assert_eq!(
parse_proc_self_cgroup(weird),
("/odd:name/x".to_string(), "/".to_string())
);
}
#[cfg(target_os = "linux")]
#[test]
fn cgroup_ancestor_paths_walk_leaf_to_root() {
assert_eq!(
cgroup_ancestor_limit_paths("/sys/fs/cgroup", "memory.max", "/a/b/c"),
vec![
"/sys/fs/cgroup/a/b/c/memory.max".to_string(),
"/sys/fs/cgroup/a/b/memory.max".to_string(),
"/sys/fs/cgroup/a/memory.max".to_string(),
"/sys/fs/cgroup/memory.max".to_string(),
]
);
assert_eq!(
cgroup_ancestor_limit_paths("/sys/fs/cgroup", "memory.max", "/"),
vec!["/sys/fs/cgroup/memory.max".to_string()]
);
assert_eq!(
cgroup_ancestor_limit_paths(
"/sys/fs/cgroup/memory",
"memory.limit_in_bytes",
"/system.slice"
),
vec![
"/sys/fs/cgroup/memory/system.slice/memory.limit_in_bytes".to_string(),
"/sys/fs/cgroup/memory/memory.limit_in_bytes".to_string(),
]
);
}
#[test]
fn zero_budget_keeps_one_entry() {
let mut cache: ModuleCache<u64, ()> = ModuleCache::new(0);
assert_eq!(cache.budget_bytes(), 1, "budget clamped up to 1");
cache.insert(0, (), 100);
cache.insert(1, (), 100);
assert_eq!(cache.len(), 1, "at most one entry survives a zero budget");
assert!(cache.get(&1).is_some(), "most-recent entry kept");
assert!(cache.get(&0).is_none());
}
#[test]
fn module_cache_metrics_routing_and_isolation() {
let m = ModuleCacheMetrics::new();
m.record_occupancy("contract", 3, 300, 1000);
m.add_evictions("contract", 2);
let s = m.snapshot();
assert_eq!(s.contract_entries, 3);
assert_eq!(s.contract_total_bytes, 300);
assert_eq!(s.contract_budget_bytes, 1000);
assert_eq!(s.contract_evictions_total, 2);
assert_eq!(s.delegate_entries, 0);
assert_eq!(s.delegate_evictions_total, 0);
m.record_occupancy("delegate", 1, 50, 250);
m.add_evictions("delegate", 5);
m.add_evictions("contract", 4);
m.record_occupancy("contract", 1, 100, 1000);
let s = m.snapshot();
assert_eq!(s.contract_evictions_total, 6, "evictions accumulate");
assert_eq!(s.contract_entries, 1, "occupancy is last-write-wins");
assert_eq!(s.delegate_entries, 1);
assert_eq!(s.delegate_evictions_total, 5);
m.record_occupancy("module", 999, 999, 999);
m.add_evictions("module", 999);
let s = m.snapshot();
assert_eq!(s.contract_entries, 1, "unknown label is a no-op");
assert_eq!(s.delegate_entries, 1, "unknown label is a no-op");
}
#[test]
fn evicting_contract_cache_publishes_to_injected_metrics() {
let metrics = Arc::new(ModuleCacheMetrics::new());
let mut cache: ModuleCache<u64, ()> =
ModuleCache::with_label(10, "contract", Some(metrics.clone()));
assert_eq!(
metrics.snapshot().contract_evictions_total,
0,
"a fresh injected sink starts at zero — no global cross-talk"
);
assert_eq!(
metrics.snapshot().contract_budget_bytes,
10,
"construction publishes the configured budget"
);
cache.insert(0, (), 8);
cache.insert(1, (), 8); assert_eq!(cache.len(), 1, "the test setup must actually evict");
let s = metrics.snapshot();
assert_eq!(
s.contract_evictions_total, 1,
"exactly one eviction recorded in this isolated sink"
);
assert_eq!(s.contract_entries, 1, "occupancy is last-write-wins");
assert_eq!(
s.delegate_evictions_total, 0,
"the delegate gauges must not bleed from the contract cache"
);
}
#[test]
fn cache_without_metrics_sink_is_a_no_op() {
let mut cache: ModuleCache<u64, ()> = ModuleCache::with_label(10, "contract", None);
cache.insert(0, (), 8);
cache.insert(1, (), 8);
assert_eq!(cache.len(), 1, "eviction still happens without a sink");
}
#[test]
fn occupancy_pct_is_none_when_budget_uninitialized() {
assert_eq!(occupancy_pct(0, 0), None);
assert_eq!(occupancy_pct(1_000, 0), None);
}
#[test]
fn occupancy_pct_computes_percentage_of_budget() {
assert_eq!(occupancy_pct(0, 1_000), Some(0));
assert_eq!(occupancy_pct(500, 1_000), Some(50));
assert_eq!(occupancy_pct(900, 1_000), Some(90));
assert_eq!(occupancy_pct(1_000, 1_000), Some(100));
assert_eq!(occupancy_pct(1_500, 1_000), Some(150));
}
#[test]
fn occupancy_pct_handles_gib_scale_without_overflow() {
let budget = 1_536u64 * 1024 * 1024;
assert_eq!(occupancy_pct(budget, budget), Some(100));
assert_eq!(occupancy_pct(900_000_000, 1_000_000_000), Some(90));
}
#[test]
fn occupancy_pct_truncates_below_the_decision_boundary() {
assert_eq!(occupancy_pct(899, 1_000), Some(89));
assert_eq!(occupancy_pct(900, 1_000), Some(90));
assert_eq!(occupancy_pct(901, 1_000), Some(90));
}
fn interest_over(
set: Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
) -> InterestPredicate<u64> {
Arc::new(move |k: &u64| set.lock().unwrap().contains(k))
}
#[test]
fn interest_tiered_flag_defaults_on_unless_explicitly_disabled() {
for v in ["0", "false", "FALSE", " no ", "Off", "oFF", "", " "] {
assert!(
!parse_interest_tiered_flag(Some(v)),
"{v:?} must disable the policy (force plain byte-LRU)"
);
}
for v in ["1", "true", "TRUE", " yes ", "On", "oN"] {
assert!(parse_interest_tiered_flag(Some(v)), "{v:?} must be truthy");
}
for v in ["2", "enable", "y"] {
assert!(
parse_interest_tiered_flag(Some(v)),
"{v:?} is not an explicit opt-out → tiered eviction stays ON (default)"
);
}
assert!(
parse_interest_tiered_flag(None),
"unset env must default ON (interest-tiered eviction)"
);
}
#[test]
fn interest_tiered_enabled_reads_env_at_construction() {
use std::sync::Mutex as StdMutex;
static ENV_GUARD: StdMutex<()> = StdMutex::new(());
let _g = ENV_GUARD.lock().unwrap_or_else(|p| p.into_inner());
let prev = std::env::var(INTEREST_TIERED_ENV).ok();
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
let pred = || interest_over(interested.clone());
unsafe { std::env::set_var(INTEREST_TIERED_ENV, "true") };
let on =
ModuleCache::<u64, ()>::with_label_and_interest(10, "contract", None, Some(pred()));
assert!(
on.interest_tiered_active(),
"flag set truthy → tiered policy active"
);
unsafe { std::env::remove_var(INTEREST_TIERED_ENV) };
let default_on =
ModuleCache::<u64, ()>::with_label_and_interest(10, "contract", None, Some(pred()));
assert!(
default_on.interest_tiered_active(),
"flag unset → tiered policy active by default (canary-validated 2026-06-28)"
);
unsafe { std::env::set_var(INTEREST_TIERED_ENV, "0") };
let opted_out =
ModuleCache::<u64, ()>::with_label_and_interest(10, "contract", None, Some(pred()));
assert!(
!opted_out.interest_tiered_active(),
"explicit falsy flag → tiered policy disabled (plain byte-LRU)"
);
unsafe { std::env::set_var(INTEREST_TIERED_ENV, "true") };
let none: ModuleCache<u64, ()> =
ModuleCache::with_label_and_interest(10, "contract", None, None);
assert!(
!none.interest_tiered_active(),
"no predicate → never tiered even with the flag on"
);
match prev {
Some(v) => unsafe { std::env::set_var(INTEREST_TIERED_ENV, v) },
None => unsafe { std::env::remove_var(INTEREST_TIERED_ENV) },
}
}
#[test]
fn interest_shadow_get_refresh_respects_throttle_window() {
let metrics = Arc::new(ModuleCacheMetrics::new());
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(0u64); let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
1_000_000,
"contract",
Some(metrics.clone()),
interest_over(interested.clone()),
false,
);
cache.insert(0, (), 6);
cache.insert(1, (), 6); assert_eq!(
metrics.snapshot().contract_evictions_would_reclassify_total,
0,
"the within-window insert must NOT refresh (throttled)"
);
let _ = cache.get(&1);
assert_eq!(
metrics.snapshot().contract_evictions_would_reclassify_total,
0,
"a within-window get must NOT refresh"
);
cache.arm_interest_shadow_refresh();
let _ = cache.get(&1);
assert_eq!(
metrics.snapshot().contract_evictions_would_reclassify_total,
1,
"after re-arming, the get refresh fires and samples the divergence"
);
}
#[test]
fn flag_off_evicts_absolute_lru_ignoring_interest() {
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(0u64); let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
10,
"contract",
None,
interest_over(interested.clone()),
false, );
assert!(!cache.interest_tiered_active());
cache.insert(0, (), 6); cache.insert(1, (), 6); assert_eq!(cache.len(), 1);
assert!(
cache.get(&0).is_none(),
"flag OFF: the interested-but-LRU entry is evicted, exactly as plain LRU"
);
assert!(cache.get(&1).is_some());
assert!(cache.total_bytes() <= cache.budget_bytes());
}
#[test]
fn flag_off_resident_set_matches_no_predicate_cache_ab() {
const BUDGET: usize = 30; let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
for k in [0u64, 2, 4, 6, 8, 10] {
interested.lock().unwrap().insert(k);
}
let mut plain: ModuleCache<u64, ()> = ModuleCache::with_label(BUDGET, "delegate", None);
let mut predicated = ModuleCache::<u64, ()>::with_interest_for_test(
BUDGET,
"contract",
None,
interest_over(interested.clone()),
false, );
let sizes = [10usize, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10];
for (k, &size) in sizes.iter().enumerate() {
let key = k as u64;
plain.insert(key, (), size);
predicated.insert(key, (), size);
if k % 3 == 0 && k > 0 {
let touch = (k as u64) - 1;
let _ = plain.get(&touch);
let _ = predicated.get(&touch);
}
assert_eq!(
plain.total_bytes(),
predicated.total_bytes(),
"total_bytes diverged after inserting key {key}"
);
assert_eq!(
plain.len(),
predicated.len(),
"resident count diverged after inserting key {key}"
);
for probe in 0..=(k as u64) {
assert_eq!(
plain.get(&probe).is_some(),
predicated.get(&probe).is_some(),
"residency of key {probe} diverged after inserting key {key} \
(flag-OFF predicate cache must match pure LRU exactly)"
);
}
}
}
#[test]
fn flag_on_evicts_cold_first_sparing_interested() {
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(0u64); let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
10,
"contract",
None,
interest_over(interested.clone()),
true, );
assert!(cache.interest_tiered_active());
cache.insert(0, (), 6); cache.insert(1, (), 6); assert_eq!(cache.len(), 1);
assert!(
cache.get(&0).is_some(),
"two-tier: interested entry is spared even as the absolute LRU"
);
assert!(
cache.get(&1).is_none(),
"two-tier: the cold entry is evicted first"
);
assert!(cache.total_bytes() <= cache.budget_bytes());
}
#[test]
fn flag_on_evicts_interested_when_interested_alone_exceeds_budget() {
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
for k in 0..3u64 {
interested.lock().unwrap().insert(k);
}
let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
10,
"contract",
None,
interest_over(interested.clone()),
true,
);
cache.insert(0, (), 6); cache.insert(1, (), 6); assert!(
cache.total_bytes() <= cache.budget_bytes(),
"byte budget is strict even when the whole working set is interested"
);
assert_eq!(cache.len(), 1);
assert!(
cache.get(&0).is_none(),
"interested LRU is evicted when interested-alone exceeds budget — \
nothing is pinned forever"
);
assert!(cache.get(&1).is_some());
}
#[test]
fn flag_on_drains_cold_then_crosses_into_interested() {
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(0u64);
interested.lock().unwrap().insert(1u64);
let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
10,
"contract",
None,
interest_over(interested.clone()),
true,
);
cache.insert(0, (), 4); cache.insert(1, (), 4); cache.remove(&0);
cache.remove(&1);
cache.insert(2, (), 4); cache.insert(3, (), 4); cache.insert(0, (), 4); assert!(cache.total_bytes() <= cache.budget_bytes());
assert!(cache.get(&2).is_none(), "cold LRU evicted first");
assert!(cache.get(&3).is_some(), "second cold spared (budget met)");
assert!(cache.get(&0).is_some(), "interested newcomer kept");
cache.insert(1, (), 4);
assert!(cache.total_bytes() <= cache.budget_bytes());
assert!(
cache.get(&3).is_none(),
"remaining cold evicted before interested"
);
assert!(cache.get(&0).is_some(), "interested entries both spared");
assert!(cache.get(&1).is_some());
}
#[test]
fn shadow_interest_bytes_published_with_flag_off() {
let metrics = Arc::new(ModuleCacheMetrics::new());
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(0u64);
let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
1_000_000,
"contract",
Some(metrics.clone()),
interest_over(interested.clone()),
false, );
cache.insert(0, (), 100); cache.insert(1, (), 250); cache.insert(2, (), 300); cache.force_refresh_interest_shadow();
let s = metrics.snapshot();
assert_eq!(s.contract_interested_bytes, 100, "only key 0 is interested");
assert_eq!(
s.contract_cold_evictable_bytes, 550,
"keys 1 + 2 are cold-evictable"
);
}
#[test]
fn shadow_would_reclassify_counts_divergence_state_with_flag_off() {
let metrics = Arc::new(ModuleCacheMetrics::new());
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(0u64);
let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
1_000_000, "contract",
Some(metrics.clone()),
interest_over(interested.clone()),
false, );
cache.insert(0, (), 6); cache.insert(1, (), 6); cache.force_refresh_interest_shadow();
assert_eq!(
metrics.snapshot().contract_evictions_would_reclassify_total,
1,
"one refresh observed the divergence state (interested LRU + a cold entry)"
);
cache.force_refresh_interest_shadow();
assert_eq!(
metrics.snapshot().contract_evictions_would_reclassify_total,
2,
"the counter samples per refresh while the divergence state persists"
);
}
#[test]
fn shadow_would_reclassify_zero_when_lru_is_cold() {
let metrics = Arc::new(ModuleCacheMetrics::new());
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(1u64);
let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
1_000_000,
"contract",
Some(metrics.clone()),
interest_over(interested.clone()),
false,
);
cache.insert(0, (), 6); cache.insert(1, (), 6); cache.force_refresh_interest_shadow();
cache.force_refresh_interest_shadow();
assert_eq!(
metrics.snapshot().contract_evictions_would_reclassify_total,
0,
"LRU is cold → policies agree → no divergence sampled"
);
}
#[test]
fn shadow_would_reclassify_zero_when_all_interested() {
let metrics = Arc::new(ModuleCacheMetrics::new());
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(0u64);
interested.lock().unwrap().insert(1u64);
let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
1_000_000,
"contract",
Some(metrics.clone()),
interest_over(interested.clone()),
false,
);
cache.insert(0, (), 6);
cache.insert(1, (), 6);
cache.force_refresh_interest_shadow();
assert_eq!(
metrics.snapshot().contract_evictions_would_reclassify_total,
0,
"no cold entry exists → two-tier would not diverge"
);
}
#[test]
fn shadow_get_hit_on_interested_lru_does_not_false_count() {
let metrics = Arc::new(ModuleCacheMetrics::new());
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(0u64);
let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
1_000_000,
"contract",
Some(metrics.clone()),
interest_over(interested.clone()),
false,
);
cache.insert(0, (), 6); cache.insert(1, (), 6); cache.arm_interest_shadow_refresh();
let _ = cache.get(&0);
assert_eq!(
metrics.snapshot().contract_evictions_would_reclassify_total,
0,
"hitting the interested LRU makes it MRU; the post-hit LRU is cold, so \
the policies agree and no divergence may be counted"
);
}
#[test]
fn no_predicate_cache_has_zero_interest_shadow() {
let metrics = Arc::new(ModuleCacheMetrics::new());
let mut cache: ModuleCache<u64, ()> =
ModuleCache::with_label(10, "delegate", Some(metrics.clone()));
assert!(
!cache.interest_tiered_active(),
"no predicate → never tiered"
);
cache.insert(0, (), 6);
cache.insert(1, (), 6);
let s = metrics.snapshot();
assert_eq!(s.contract_cold_evictable_bytes, 0);
assert_eq!(s.contract_interested_bytes, 0);
}
#[test]
fn contract_cache_interested_occupancy_pct_reads_interested_over_budget() {
let m = ModuleCacheMetrics::with_contract_gauges_for_test(980, 1000, 300);
assert_eq!(contract_cache_interested_occupancy_pct(&m), Some(30));
assert_eq!(contract_cache_occupancy_pct(&m), Some(98));
let cold = ModuleCacheMetrics::with_contract_gauges_for_test(980, 1000, 0);
assert_eq!(contract_cache_interested_occupancy_pct(&cold), Some(0));
let fresh = ModuleCacheMetrics::new();
assert_eq!(contract_cache_interested_occupancy_pct(&fresh), None);
}
#[test]
fn migration_admission_recovered_logic() {
const CEIL: u64 = 90;
assert!(migration_admission_recovered(
950, 100, 1000, CEIL
));
assert!(!migration_admission_recovered(950, 950, 1000, CEIL));
assert!(!migration_admission_recovered(500, 100, 1000, CEIL));
assert!(!migration_admission_recovered(950, 100, 0, CEIL));
assert!(migration_admission_recovered(900, 100, 1000, CEIL));
}
#[test]
fn migration_admission_recovered_counter_accumulates() {
let m = ModuleCacheMetrics::new();
assert_eq!(m.snapshot().migration_admission_recovered_total, 0);
m.record_migration_admission_recovered();
m.record_migration_admission_recovered();
assert_eq!(m.snapshot().migration_admission_recovered_total, 2);
}
#[test]
fn metrics_refresher_weak_handle_is_fresh_when_alive_and_noop_when_dropped() {
let metrics = Arc::new(ModuleCacheMetrics::new());
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(0u64);
let cache = Arc::new(std::sync::Mutex::new(
ModuleCache::<u64, ()>::with_interest_for_test(
1_000_000,
"contract",
Some(metrics.clone()),
interest_over(interested.clone()),
false,
),
));
cache.lock().unwrap().insert(0, (), 100); cache.lock().unwrap().insert(1, (), 250); cache.lock().unwrap().force_refresh_interest_shadow();
assert_eq!(metrics.snapshot().contract_interested_bytes, 100);
let weak = Arc::downgrade(&cache);
metrics.set_interest_shadow_refresher(Arc::new(move || {
if let Some(c) = weak.upgrade() {
if let Ok(mut c) = c.lock() {
c.force_refresh_interest_shadow();
}
}
}));
interested.lock().unwrap().remove(&0);
assert_eq!(
metrics.snapshot().contract_interested_bytes,
100,
"stale until the on-demand refresh runs"
);
metrics.refresh_interest_shadow_now();
let s = metrics.snapshot();
assert_eq!(
s.contract_interested_bytes, 0,
"on-demand refresh makes the emitted snapshot fresh on an idle cache"
);
assert_eq!(s.contract_cold_evictable_bytes, 350);
drop(cache);
metrics.refresh_interest_shadow_now(); assert_eq!(metrics.snapshot().contract_interested_bytes, 0);
}
#[test]
fn interest_shadow_refreshes_on_get_after_interest_flip() {
let metrics = Arc::new(ModuleCacheMetrics::new());
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(0u64);
let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
1_000_000,
"contract",
Some(metrics.clone()),
interest_over(interested.clone()),
false,
);
cache.insert(0, (), 100); cache.insert(1, (), 250); cache.force_refresh_interest_shadow();
let s = metrics.snapshot();
assert_eq!(s.contract_interested_bytes, 100);
assert_eq!(s.contract_cold_evictable_bytes, 250);
interested.lock().unwrap().remove(&0);
assert_eq!(metrics.snapshot().contract_interested_bytes, 100);
cache.arm_interest_shadow_refresh();
let _ = cache.get(&1);
let s = metrics.snapshot();
assert_eq!(
s.contract_interested_bytes, 0,
"interest flip with no cache mutation must be picked up on the next get"
);
assert_eq!(
s.contract_cold_evictable_bytes, 350,
"both entries are now cold-evictable"
);
}
#[test]
fn two_tier_keeps_hot_set_resident_under_cold_churn() {
let budget = 30usize;
let hot: [u64; 2] = [1000, 1001];
let interested = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
interested.lock().unwrap().insert(hot[0]);
interested.lock().unwrap().insert(hot[1]);
let run = |tiered: bool| -> u32 {
let mut cache = ModuleCache::<u64, ()>::with_interest_for_test(
budget,
"contract",
None,
interest_over(interested.clone()),
tiered,
);
cache.insert(hot[0], (), 10);
cache.insert(hot[1], (), 10);
let mut hot_misses = 0u32;
let mut cold_key = 0u64;
for _round in 0..6 {
for _ in 0..3 {
cache.insert(cold_key, (), 10);
cold_key += 1;
}
for &h in &hot {
if cache.get(&h).is_none() {
hot_misses += 1;
cache.insert(h, (), 10);
}
}
}
hot_misses
};
let tiered_misses = run(true);
let lru_misses = run(false);
assert_eq!(
tiered_misses, 0,
"two-tier policy must keep the hot interested set resident — zero recompiles"
);
assert!(
lru_misses > 0,
"plain byte-LRU evicts the hot set under cold churn (the #4441 thrash); \
tiered={tiered_misses} lru={lru_misses}"
);
}
}