/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin */ mod helper; use anyhow::{Context, Result, anyhow}; pub use helper::{EscapeKey, HashKey}; use jellykv::BlobStorage; use log::{info, warn}; use serde::{Deserialize, Serialize}; use std::{ any::Any, collections::{BTreeMap, HashMap}, hash::{DefaultHasher, Hash, Hasher}, sync::{ Arc, LazyLock, Mutex, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering}, }, time::Instant, }; const CACHE_GENERATION_BUCKET_COUNT: usize = 1024; pub static CACHE_GENERATION_LOCKS: LazyLock<[Mutex<()>; CACHE_GENERATION_BUCKET_COUNT]> = LazyLock::new(|| [(); CACHE_GENERATION_BUCKET_COUNT].map(|_| Mutex::new(()))); thread_local! { pub static WITHIN_CACHE_FILE: AtomicBool = const { AtomicBool::new(false) }; } pub struct Cache { storage: Box, memory_cache: RwLock>, memory_cache_size: AtomicUsize, max_memory_cache_size: usize, } pub struct InMemoryCacheEntry { size: usize, last_access: Instant, object: Arc, } fn bucket(key: &str) -> usize { let mut h = DefaultHasher::new(); key.hash(&mut h); h.finish() as usize % CACHE_GENERATION_BUCKET_COUNT } impl Cache { pub fn new(storage: Box, max_memory_cache_size: usize) -> Self { Self { max_memory_cache_size, storage, memory_cache: HashMap::new().into(), memory_cache_size: AtomicUsize::new(0), } } pub fn cache(&self, key: &str, generate: impl FnOnce() -> Result>) -> Result> { // we need a lock even if it exists since somebody might be still in the process of writing. let already_within = WITHIN_CACHE_FILE.with(|a| a.swap(true, Ordering::Relaxed)); let _guard = if already_within { // TODO stupid hack to avoid deadlock for nested calls; not locking is fine but might cause double-generating CACHE_GENERATION_LOCKS[bucket(key)].try_lock().ok() } else { CACHE_GENERATION_LOCKS[bucket(key)].lock().ok() }; let out = match self.storage.read(key)? { Some(x) => x, None => { let value = generate()?; self.storage.store(key, &value)?; value } }; if !already_within { WITHIN_CACHE_FILE.with(|a| a.swap(false, Ordering::Relaxed)); } drop(_guard); Ok(out) } pub fn read(&self, key: &str) -> Result>> { self.storage.read(key) } pub fn store(&self, key: String, generate: impl FnOnce() -> Result>) -> Result { self.cache(&key, generate)?; Ok(key) } pub fn cache_memory( &self, key: &str, mut generate: Fun, ) -> Result, anyhow::Error> where Fun: FnMut() -> Result, T: Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static, { if !key.ends_with(".json") { warn!("cache_memory key not ending in .json: {key:?}") } { let mut g = self.memory_cache.write().unwrap(); if let Some(entry) = g.get_mut(key) { entry.last_access = Instant::now(); let object = entry .object .clone() .downcast::() .map_err(|_| anyhow!("inconsistent types for in-memory cache"))?; return Ok(object); } } let data = self.cache(key, move || { let object = generate()?; Ok(serde_json::to_vec(&object)?) })?; let size = data.len(); let object = serde_json::from_slice::(&data).context("decoding cache object")?; let object = Arc::new(object); { let mut g = self.memory_cache.write().unwrap(); g.insert( key.to_owned(), InMemoryCacheEntry { size, last_access: Instant::now(), object: object.clone(), }, ); self.memory_cache_size.fetch_add(size, Ordering::Relaxed); } self.cleanup_cache(); Ok(object) } fn cleanup_cache(&self) { let current_size = self.memory_cache_size.load(Ordering::Relaxed); if current_size < self.max_memory_cache_size { return; } info!("running cache eviction"); let mut g = self.memory_cache.write().unwrap(); // TODO: if two entries have *exactly* the same size, only one of the will be remove; this is fine for now let mut k = BTreeMap::new(); for (loc, entry) in g.iter() { k.insert(entry.last_access.elapsed(), (loc.to_owned(), entry.size)); } let mut reduction = 0; for (loc, size) in k.values().rev().take(k.len().div_ceil(2)) { g.remove(loc); reduction += size; } self.memory_cache_size .fetch_sub(reduction, Ordering::Relaxed); drop(g); info!( "done, {} freed", humansize::format_size(reduction, humansize::DECIMAL) ); } }