pub mod coherence;
#[cfg(feature = "maud")]
mod fragment;
mod layer;
#[cfg(feature = "cache-moka")]
mod moka_impl;
mod read_through;
#[cfg(feature = "maud")]
pub use fragment::{
cache_fragment, cache_fragment_global, cache_fragment_global_in, cache_fragment_in,
};
pub use layer::{CacheResponseLayer, CacheResponseService};
#[cfg(feature = "cache-moka")]
pub use moka_impl::MokaCache;
pub use read_through::{
CacheFillError, GetOrComputeOptions, ReadThroughMetrics, ReadThroughMetricsSnapshot,
get_or_compute, get_or_compute_with, jittered_ttl, read_through_metrics,
};
use std::any::Any;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::{Arc, RwLock};
use std::time::Duration;
static GLOBAL_CACHE: RwLock<Option<Arc<dyn Cache>>> = RwLock::new(None);
pub fn set_global_cache(cache: Arc<dyn Cache>) {
*GLOBAL_CACHE.write().expect("global cache lock poisoned") = Some(cache);
}
#[must_use]
pub fn global_cache() -> Option<Arc<dyn Cache>> {
GLOBAL_CACHE
.read()
.expect("global cache lock poisoned")
.clone()
}
pub fn clear_global_cache() {
*GLOBAL_CACHE.write().expect("global cache lock poisoned") = None;
}
pub(crate) static GLOBAL_CACHE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[derive(Clone)]
pub struct RawCacheBytes(pub Vec<u8>);
pub trait Cache: Send + Sync + 'static {
fn get_value(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>>;
fn insert_value(&self, key: &str, value: Arc<dyn Any + Send + Sync>);
fn invalidate(&self, key: &str);
fn clear(&self);
fn invalidate_namespace(&self, _namespace: &str) -> bool {
false
}
fn insert_raw_bytes(&self, _key: &str, _bytes: Vec<u8>, _ttl: Option<std::time::Duration>) {}
fn try_acquire_fill_lock(&self, _key: &str, _token: &str, _ttl: Duration) -> FillLockStatus {
FillLockStatus::Unsupported
}
fn release_fill_lock(&self, _key: &str, _token: &str) {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FillLockStatus {
Acquired,
Held,
Unsupported,
}
pub fn get<V: Clone + Send + Sync + 'static>(cache: &dyn Cache, key: &str) -> Option<V> {
cache
.get_value(key)
.and_then(|arc| arc.downcast_ref::<V>().cloned())
}
pub fn insert<V: Clone + Send + Sync + 'static>(cache: &dyn Cache, key: &str, value: V) {
cache.insert_value(key, Arc::new(value));
}
pub fn get_cached<V>(cache: &dyn Cache, key: &str) -> Option<V>
where
V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
{
match replayed_cache_get::<V>(key) {
ReplayedRead::NoTape => {}
ReplayedRead::Miss => return None,
ReplayedRead::Hit(value) => return Some(value),
}
let arc = cache.get_value(key);
let value = arc.and_then(|arc| {
if let Some(value) = arc.downcast_ref::<V>() {
return Some(value.clone());
}
arc.downcast_ref::<RawCacheBytes>()
.and_then(|raw| serde_json::from_slice::<V>(&raw.0).ok())
});
record_cache_get(key, value.as_ref());
value
}
pub fn insert_cached<V>(cache: &dyn Cache, key: &str, value: V, ttl: Option<std::time::Duration>)
where
V: Clone + serde::Serialize + Send + Sync + 'static,
{
let bytes = serde_json::to_vec(&value).ok();
if record_or_replay_cache_insert(key, bytes.as_deref(), ttl) {
return;
}
cache.insert_value(key, Arc::new(value));
if let Some(bytes) = bytes {
cache.insert_raw_bytes(key, bytes, ttl);
}
}
enum ReplayedRead<V> {
NoTape,
Miss,
Hit(V),
}
#[cfg(feature = "reporting")]
fn replayed_cache_get<V>(key: &str) -> ReplayedRead<V>
where
V: Clone + serde::de::DeserializeOwned + Send + Sync + 'static,
{
use crate::capsule::effects::CachedValue;
let Some(tape) = crate::capsule::effects::current_tape() else {
return ReplayedRead::NoTape;
};
match tape.cache_get(key) {
CachedValue::Hit(bytes) => {
serde_json::from_slice::<V>(&bytes).map_or(ReplayedRead::Miss, ReplayedRead::Hit)
}
CachedValue::Miss | CachedValue::Unrecorded => ReplayedRead::Miss,
}
}
#[cfg(not(feature = "reporting"))]
const fn replayed_cache_get<V>(_key: &str) -> ReplayedRead<V>
where
V: Clone + serde::de::DeserializeOwned + Send + Sync + 'static,
{
ReplayedRead::NoTape
}
#[cfg(feature = "reporting")]
fn record_cache_get<V: serde::Serialize>(key: &str, value: Option<&V>) {
let Some(scope) = crate::capsule::current_scope() else {
return;
};
let encoded = value.map(|value| {
serde_json::to_vec(value).map(|bytes| {
use base64::Engine as _;
base64::engine::general_purpose::STANDARD.encode(&bytes)
})
});
let encoded = match encoded {
None => None,
Some(Ok(encoded)) => Some(encoded),
Some(Err(_)) => {
scope.note(UNRECORDABLE_CACHE_HIT_NOTE);
scope.mark_truncated();
None
}
};
scope.record_cache(crate::capsule::CacheEffect::Get {
key: key.to_owned(),
value: encoded,
});
}
#[cfg(feature = "reporting")]
const UNRECORDABLE_CACHE_WRITE_NOTE: &str = "a cache write held a value that could not be serialized into the capsule, so replay would \
suppress a mutation the recorded run really made";
#[cfg(feature = "reporting")]
const UNRECORDABLE_CACHE_HIT_NOTE: &str = "a cache read hit a value that could not be serialized into the capsule, and a hit with no \
recorded value is indistinguishable from a miss; replay would take the miss branch the \
recorded run never took";
#[cfg(not(feature = "reporting"))]
const fn record_cache_get<V: serde::Serialize>(_key: &str, _value: Option<&V>) {}
#[cfg(feature = "reporting")]
fn record_or_replay_cache_insert(
key: &str,
bytes: Option<&[u8]>,
ttl: Option<std::time::Duration>,
) -> bool {
if let Some(tape) = crate::capsule::effects::current_tape() {
if let Some(bytes) = bytes {
tape.cache_insert(key, bytes, ttl.map(|ttl| ttl.as_secs()));
}
return true;
}
if let Some(scope) = crate::capsule::current_scope() {
if let Some(bytes) = bytes {
use base64::Engine as _;
scope.record_cache(crate::capsule::CacheEffect::Insert {
key: key.to_owned(),
value: base64::engine::general_purpose::STANDARD.encode(bytes),
ttl_secs: ttl.map(|ttl| ttl.as_secs()),
});
} else {
scope.note(UNRECORDABLE_CACHE_WRITE_NOTE);
scope.mark_truncated();
}
}
false
}
#[cfg(not(feature = "reporting"))]
const fn record_or_replay_cache_insert(
_key: &str,
_bytes: Option<&[u8]>,
_ttl: Option<std::time::Duration>,
) -> bool {
false
}
pub trait CacheableResult {
type Ok: Clone;
type Err;
fn into_result(self) -> Result<Self::Ok, Self::Err>;
fn from_ok(ok: Self::Ok) -> Self;
}
impl<T: Clone, E> CacheableResult for Result<T, E> {
type Ok = T;
type Err = E;
fn into_result(self) -> Self {
self
}
fn from_ok(ok: T) -> Self {
Ok(ok)
}
}
#[must_use]
pub fn make_cache_key<K: Hash>(fn_name: &str, args: &K) -> String {
let mut hasher = DefaultHasher::new();
args.hash(&mut hasher);
format!("{}:{:x}", fn_name, hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_key_deterministic() {
let k1 = make_cache_key("get_user", &(42_i64,));
let k2 = make_cache_key("get_user", &(42_i64,));
assert_eq!(k1, k2);
}
#[test]
fn cache_key_differs_by_fn_name() {
let k1 = make_cache_key("get_user", &(42_i64,));
let k2 = make_cache_key("find_user", &(42_i64,));
assert_ne!(k1, k2);
}
#[test]
fn cache_key_differs_by_args() {
let k1 = make_cache_key("get_user", &(1_i64,));
let k2 = make_cache_key("get_user", &(2_i64,));
assert_ne!(k1, k2);
}
#[test]
fn cache_key_no_args() {
let k = make_cache_key("get_config", &());
assert!(k.starts_with("get_config:"));
}
#[cfg(feature = "cache-moka")]
#[test]
fn insert_cached_and_get_cached_round_trip() {
let cache = MokaCache::new(10, None);
insert_cached(&cache, "key", "hello".to_string(), None);
let val: Option<String> = get_cached(&cache, "key");
assert_eq!(val.as_deref(), Some("hello"));
}
#[cfg(feature = "cache-moka")]
#[test]
fn get_cached_raw_bytes_slow_path() {
let cache = MokaCache::new(10, None);
let bytes = serde_json::to_vec(&42_i32).unwrap();
cache.insert_value("k", Arc::new(RawCacheBytes(bytes)));
let val: Option<i32> = get_cached(&cache, "k");
assert_eq!(val, Some(42));
}
#[cfg(feature = "cache-moka")]
#[test]
fn get_cached_miss_returns_none() {
let cache = MokaCache::new(10, None);
let val: Option<String> = get_cached(&cache, "missing");
assert!(val.is_none());
}
#[test]
fn cacheable_result_ok_round_trips() {
let r: Result<i32, &str> = Result::from_ok(42);
assert_eq!(r, Ok(42));
assert_eq!(r.into_result(), Ok(42));
}
#[test]
fn cacheable_result_err_passes_through() {
let r: Result<i32, &str> = Err("oops");
assert_eq!(r.into_result(), Err("oops"));
}
}