use std::sync::{Arc, RwLock};
use pointbreak::session::{BaseHistoryProjection, BaseProjectionConfig, TrustSet};
pub(super) type HistoryProjectionCache = MarkerCache<HistoryCacheKey, BaseHistoryProjection>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct HistoryCacheKey {
pub(super) marker: u64,
pub(super) config: BaseProjectionConfig,
}
pub(super) type RevisionsResponseCache = MarkerCache<RevisionsCacheKey, String>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct RevisionsCacheKey {
pub(super) marker: u64,
pub(super) trust_set: TrustSet,
pub(super) commit_graph_stamp: String,
}
pub(super) struct MarkerCache<K, T> {
slot: RwLock<Option<Cached<K, T>>>,
}
struct Cached<K, T> {
key: K,
value: Arc<T>,
}
impl<K: PartialEq, T> MarkerCache<K, T> {
pub(super) fn new() -> Self {
Self {
slot: RwLock::new(None),
}
}
pub(super) fn get_or_build(
&self,
key: K,
build: impl FnOnce(&K) -> Result<T, String>,
) -> Result<Arc<T>, String> {
if let Some(cached) = self.slot.read().unwrap().as_ref()
&& cached.key == key
{
return Ok(Arc::clone(&cached.value));
}
let mut guard = self.slot.write().unwrap();
if let Some(cached) = guard.as_ref()
&& cached.key == key
{
return Ok(Arc::clone(&cached.value));
}
let value = Arc::new(build(&key)?);
*guard = Some(Cached {
key,
value: Arc::clone(&value),
});
Ok(value)
}
pub(super) fn try_get(&self, key: &K) -> Option<Arc<T>> {
let guard = self.slot.try_read().ok()?;
let cached = guard.as_ref()?;
(cached.key == *key).then(|| Arc::clone(&cached.value))
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
fn base_stub(tag: &str) -> BaseHistoryProjection {
BaseHistoryProjection {
entries: Vec::new(),
event_set_hash: tag.to_owned(),
event_count: 0,
diagnostics: Vec::new(),
}
}
#[test]
fn builds_once_and_reuses_on_unchanged_marker() {
let cache = MarkerCache::<u64, BaseHistoryProjection>::new();
let builds = AtomicUsize::new(0);
let build = |_key: &u64| {
builds.fetch_add(1, Ordering::SeqCst);
Ok(base_stub("v1"))
};
let a = cache.get_or_build(7, build).unwrap();
let b = cache.get_or_build(7, build).unwrap();
assert_eq!(
builds.load(Ordering::SeqCst),
1,
"same marker -> built once"
);
assert!(Arc::ptr_eq(&a, &b), "same Arc reused");
}
#[test]
fn rebuilds_when_marker_changes() {
let cache = MarkerCache::<u64, BaseHistoryProjection>::new();
let builds = AtomicUsize::new(0);
let v1 = cache
.get_or_build(7, |_| {
builds.fetch_add(1, Ordering::SeqCst);
Ok(base_stub("v1"))
})
.unwrap();
let v2 = cache
.get_or_build(8, |_| {
builds.fetch_add(1, Ordering::SeqCst);
Ok(base_stub("v2"))
})
.unwrap();
assert_eq!(
builds.load(Ordering::SeqCst),
2,
"changed marker -> rebuilt"
);
assert!(!Arc::ptr_eq(&v1, &v2));
assert_eq!(v2.event_set_hash, "v2");
}
#[test]
fn build_error_is_not_cached() {
let cache = MarkerCache::<u64, BaseHistoryProjection>::new();
assert!(cache.get_or_build(7, |_| Err("boom".to_owned())).is_err());
let ok = cache.get_or_build(7, |_| Ok(base_stub("v1")));
assert!(ok.is_ok());
}
#[test]
fn try_get_returns_matching_cached_base_without_building() {
let cache = MarkerCache::<u64, BaseHistoryProjection>::new();
let built = cache.get_or_build(7, |_| Ok(base_stub("v1"))).unwrap();
let hit = cache.try_get(&7).expect("matching marker hits");
assert!(Arc::ptr_eq(&built, &hit));
assert!(cache.try_get(&8).is_none(), "stale marker misses");
}
fn revisions_key(marker: u64, stamp: &str) -> RevisionsCacheKey {
RevisionsCacheKey {
marker,
trust_set: TrustSet::default(),
commit_graph_stamp: stamp.to_owned(),
}
}
#[test]
fn revisions_cache_reuses_only_on_identical_marker_and_ref_state() {
let cache = RevisionsResponseCache::new();
let v1 = cache
.get_or_build(revisions_key(1, "stamp-a"), |_| {
Ok("{\"entries\":[]}".to_owned())
})
.unwrap();
let hit = cache
.get_or_build(revisions_key(1, "stamp-a"), |_| {
panic!("identical key must not rebuild")
})
.unwrap();
assert!(Arc::ptr_eq(&v1, &hit));
let restamped = cache
.get_or_build(revisions_key(1, "stamp-b"), |_| {
Ok("{\"entries\":[\"landed\"]}".to_owned())
})
.unwrap();
assert_eq!(*restamped, "{\"entries\":[\"landed\"]}");
assert!(
cache.try_get(&revisions_key(1, "stamp-a")).is_none(),
"the single slot now holds the re-stamped payload"
);
let v2 = cache
.get_or_build(revisions_key(2, "stamp-b"), |_| {
Ok("{\"entries\":[1]}".to_owned())
})
.unwrap();
assert_eq!(*v2, "{\"entries\":[1]}");
}
}