use std::sync::{Arc, RwLock};
use pointbreak::session::BaseHistoryProjection;
pub(super) struct HistoryProjectionCache {
slot: RwLock<Option<Cached>>,
}
struct Cached {
marker: u64,
base: Arc<BaseHistoryProjection>,
}
impl HistoryProjectionCache {
pub(super) fn new() -> Self {
Self {
slot: RwLock::new(None),
}
}
pub(super) fn get_or_build(
&self,
marker: u64,
build: impl FnOnce() -> Result<BaseHistoryProjection, String>,
) -> Result<Arc<BaseHistoryProjection>, String> {
if let Some(cached) = self.slot.read().unwrap().as_ref()
&& cached.marker == marker
{
return Ok(Arc::clone(&cached.base));
}
let mut guard = self.slot.write().unwrap();
if let Some(cached) = guard.as_ref()
&& cached.marker == marker
{
return Ok(Arc::clone(&cached.base));
}
let base = Arc::new(build()?);
*guard = Some(Cached {
marker,
base: Arc::clone(&base),
});
Ok(base)
}
}
#[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 = HistoryProjectionCache::new();
let builds = AtomicUsize::new(0);
let build = || {
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 = HistoryProjectionCache::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 = HistoryProjectionCache::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());
}
}