use std::collections::HashMap;
use std::hash::Hash;
use std::sync::{Arc, Mutex, OnceLock};
pub struct OnceCache<K: Eq + Hash + Clone, V: Clone> {
inner: Mutex<HashMap<K, Arc<OnceLock<Result<V, String>>>>>,
}
impl<K: Eq + Hash + Clone, V: Clone> Default for OnceCache<K, V> {
fn default() -> Self {
Self::new()
}
}
impl<K: Eq + Hash + Clone, V: Clone> OnceCache<K, V> {
pub fn new() -> Self {
Self { inner: Mutex::new(HashMap::new()) }
}
pub fn get_or_init<F>(&self, key: K, init: F) -> Result<V, String>
where F: FnOnce() -> Result<V, String>
{
let slot: Arc<OnceLock<Result<V, String>>> = {
let mut map = self.inner.lock().unwrap_or_else(|e| e.into_inner());
map.entry(key).or_insert_with(|| Arc::new(OnceLock::new())).clone()
};
slot.get_or_init(init).clone()
}
pub fn len(&self) -> usize {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;
#[test]
fn first_caller_runs_loader_subsequent_callers_reuse_value() {
let cache: OnceCache<&'static str, Arc<String>> = OnceCache::new();
let calls = AtomicU32::new(0);
let v1 = cache.get_or_init("k", || {
calls.fetch_add(1, Ordering::Relaxed);
Ok(Arc::new("loaded".into()))
}).unwrap();
let v2 = cache.get_or_init("k", || {
calls.fetch_add(1, Ordering::Relaxed);
Ok(Arc::new("DIFFERENT".into()))
}).unwrap();
assert_eq!(*v1, "loaded");
assert_eq!(*v2, "loaded");
assert!(Arc::ptr_eq(&v1, &v2), "second caller should see the cached arc");
assert_eq!(calls.load(Ordering::Relaxed), 1);
}
#[test]
fn concurrent_callers_share_one_loader_run() {
let cache: Arc<OnceCache<&'static str, Arc<String>>> = Arc::new(OnceCache::new());
let calls = Arc::new(AtomicU32::new(0));
let handles: Vec<_> = (0..32).map(|_| {
let cache = cache.clone();
let calls = calls.clone();
thread::spawn(move || {
cache.get_or_init("hot-key", || {
calls.fetch_add(1, Ordering::Relaxed);
thread::sleep(std::time::Duration::from_millis(5));
Ok(Arc::new("only-once".into()))
})
})
}).collect();
for h in handles {
let v = h.join().unwrap().unwrap();
assert_eq!(*v, "only-once");
}
assert_eq!(calls.load(Ordering::Relaxed), 1,
"loader should run exactly once across all concurrent callers");
}
#[test]
fn distinct_keys_load_independently() {
let cache: OnceCache<u32, u32> = OnceCache::new();
let v1 = cache.get_or_init(1, || Ok(10)).unwrap();
let v2 = cache.get_or_init(2, || Ok(20)).unwrap();
assert_eq!(v1, 10);
assert_eq!(v2, 20);
assert_eq!(cache.len(), 2);
}
#[test]
fn failed_load_is_sticky() {
let cache: OnceCache<&'static str, u32> = OnceCache::new();
let calls = AtomicU32::new(0);
let r1 = cache.get_or_init("bad", || {
calls.fetch_add(1, Ordering::Relaxed);
Err("nope".into())
});
let r2 = cache.get_or_init("bad", || {
calls.fetch_add(1, Ordering::Relaxed);
Ok(42)
});
assert_eq!(r1.unwrap_err(), "nope");
assert_eq!(r2.unwrap_err(), "nope");
assert_eq!(calls.load(Ordering::Relaxed), 1,
"second caller must NOT retry; the failure is sticky");
}
}