use futures::future::{BoxFuture, FutureExt, Shared};
use std::any::Any;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
tokio::task_local! {
pub(crate) static MEMO_STORE: Arc<MemoStore>;
}
pub type MemoSlot = Shared<BoxFuture<'static, Arc<dyn Any + Send + Sync>>>;
#[derive(Eq, PartialEq, Hash, Clone, Copy)]
pub struct MemoKey {
callsite: std::any::TypeId,
args_hash: u64,
}
impl MemoKey {
pub fn new<Marker: 'static, A: std::hash::Hash>(args: &A) -> Self {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let mut h = DefaultHasher::new();
args.hash(&mut h);
Self {
callsite: std::any::TypeId::of::<Marker>(),
args_hash: h.finish(),
}
}
}
pub struct MemoStore {
entries: Mutex<HashMap<MemoKey, MemoSlot>>,
}
impl MemoStore {
pub fn new() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
}
}
pub fn get_or_insert(
&self,
key: MemoKey,
make_fut: impl FnOnce() -> BoxFuture<'static, Arc<dyn Any + Send + Sync>>,
) -> MemoSlot {
let slot = {
let mut map = self.entries.lock().unwrap();
map.entry(key)
.or_insert_with(|| make_fut().shared())
.clone()
}; slot
}
}
impl Default for MemoStore {
fn default() -> Self {
Self::new()
}
}
pub fn current_memo_store() -> Option<Arc<MemoStore>> {
MEMO_STORE.try_with(|s| s.clone()).ok()
}
#[allow(dead_code)]
pub(crate) fn memo_scope() -> Arc<MemoStore> {
Arc::new(MemoStore::new())
}
#[allow(dead_code)]
pub(crate) async fn with_memo_scope<F, R>(store: Arc<MemoStore>, f: F) -> R
where
F: std::future::Future<Output = R>,
{
MEMO_STORE.scope(store, f).await
}
#[cfg(test)]
mod macro_tests;
#[cfg(all(test, feature = "projections"))]
mod render_path_tests;
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[tokio::test]
async fn hit_body_runs_once_for_same_key() {
let counter = Arc::new(AtomicUsize::new(0));
let store = Arc::new(MemoStore::new());
struct Marker;
let key = MemoKey::new::<Marker, _>(&42u32);
let c1 = counter.clone();
let slot1 = store.get_or_insert(key, move || {
Box::pin(async move {
c1.fetch_add(1, Ordering::SeqCst);
Arc::new(99u32) as Arc<dyn Any + Send + Sync>
})
});
let c2 = counter.clone();
let slot2 = store.get_or_insert(key, move || {
Box::pin(async move {
c2.fetch_add(100, Ordering::SeqCst);
Arc::new(0u32) as Arc<dyn Any + Send + Sync>
})
});
let a1 = slot1.await;
let a2 = slot2.await;
assert_eq!(*a1.downcast_ref::<u32>().unwrap(), 99);
assert_eq!(*a2.downcast_ref::<u32>().unwrap(), 99);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn miss_distinct_keys_each_run_body() {
let counter = Arc::new(AtomicUsize::new(0));
let store = Arc::new(MemoStore::new());
struct MarkerA;
struct MarkerB;
let key_a = MemoKey::new::<MarkerA, _>(&1u32);
let key_b = MemoKey::new::<MarkerB, _>(&1u32);
let ca = counter.clone();
let sa = store.get_or_insert(key_a, move || {
Box::pin(async move {
ca.fetch_add(1, Ordering::SeqCst);
Arc::new(10u32) as Arc<dyn Any + Send + Sync>
})
});
let cb = counter.clone();
let sb = store.get_or_insert(key_b, move || {
Box::pin(async move {
cb.fetch_add(1, Ordering::SeqCst);
Arc::new(20u32) as Arc<dyn Any + Send + Sync>
})
});
let va = sa.await;
let vb = sb.await;
assert_eq!(*va.downcast_ref::<u32>().unwrap(), 10);
assert_eq!(*vb.downcast_ref::<u32>().unwrap(), 20);
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn coalesce_concurrent_callers_run_body_once() {
let counter = Arc::new(AtomicUsize::new(0));
let store = Arc::new(MemoStore::new());
struct Marker;
let key = MemoKey::new::<Marker, _>(&7u32);
let c1 = counter.clone();
let slot1 = store.get_or_insert(key, move || {
Box::pin(async move {
c1.fetch_add(1, Ordering::SeqCst);
Arc::new(42u32) as Arc<dyn Any + Send + Sync>
})
});
let slot2 = store.get_or_insert(key, || {
Box::pin(async move { Arc::new(0u32) as Arc<dyn Any + Send + Sync> })
});
let (r1, r2) = tokio::join!(slot1, slot2);
assert_eq!(*r1.downcast_ref::<u32>().unwrap(), 42);
assert_eq!(*r2.downcast_ref::<u32>().unwrap(), 42);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[test]
fn out_of_scope_returns_none_without_panic() {
let result = current_memo_store();
assert!(result.is_none());
}
#[tokio::test]
async fn err_cached_result_returning_future() {
let counter = Arc::new(AtomicUsize::new(0));
let store = Arc::new(MemoStore::new());
struct Marker;
let key = MemoKey::new::<Marker, _>(&0u32);
let c1 = counter.clone();
let slot1 = store.get_or_insert(key, move || {
Box::pin(async move {
c1.fetch_add(1, Ordering::SeqCst);
let result: Result<u32, String> = Err("boom".to_string());
Arc::new(result) as Arc<dyn Any + Send + Sync>
})
});
let slot2 = store.get_or_insert(key, || {
Box::pin(async move { Arc::new(Ok::<u32, String>(0)) as Arc<dyn Any + Send + Sync> })
});
let a1 = slot1.await;
let a2 = slot2.await;
let r1 = a1.downcast_ref::<Result<u32, String>>().unwrap();
let r2 = a2.downcast_ref::<Result<u32, String>>().unwrap();
assert!(r1.is_err());
assert_eq!(r1.as_ref().unwrap_err(), "boom");
assert!(r2.is_err());
assert_eq!(r2.as_ref().unwrap_err(), "boom");
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn dropped_store_has_no_prior_entries() {
struct Marker;
let key = MemoKey::new::<Marker, _>(&5u32);
let store1 = Arc::new(MemoStore::new());
{
let slot = store1.get_or_insert(key, || {
Box::pin(async move { Arc::new(123u32) as Arc<dyn Any + Send + Sync> })
});
let _ = slot.await;
}
{
let map = store1.entries.lock().unwrap();
assert!(map.contains_key(&key));
}
let store2 = Arc::new(MemoStore::new());
{
let map = store2.entries.lock().unwrap();
assert!(!map.contains_key(&key));
}
}
#[tokio::test]
async fn with_scope_makes_current_memo_store_return_some() {
let store = memo_scope();
let result = with_memo_scope(store, async { current_memo_store() }).await;
assert!(result.is_some());
}
}