use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use super::TypeSchema;
pub struct CompiledCache<T> {
entries: Mutex<HashMap<String, std::sync::Arc<T>>>,
compilations: AtomicU64,
}
impl<T> Default for CompiledCache<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> CompiledCache<T> {
pub fn new() -> CompiledCache<T> {
CompiledCache {
entries: Mutex::new(HashMap::new()),
compilations: AtomicU64::new(0),
}
}
pub fn get_or_compile<E>(
&self,
schema: &TypeSchema,
build: impl FnOnce(&TypeSchema) -> Result<T, E>,
) -> Result<std::sync::Arc<T>, E> {
let key = schema.hash();
if let Some(key) = key {
let entries = self.entries.lock().expect("compiled cache lock");
if let Some(hit) = entries.get(key) {
return Ok(std::sync::Arc::clone(hit));
}
}
self.compilations.fetch_add(1, Ordering::Relaxed);
let compiled = std::sync::Arc::new(build(schema)?);
if let Some(key) = key {
let mut entries = self.entries.lock().expect("compiled cache lock");
entries.insert(key.to_string(), std::sync::Arc::clone(&compiled));
}
Ok(compiled)
}
pub fn compilations(&self) -> u64 {
self.compilations.load(Ordering::Relaxed)
}
pub fn len(&self) -> usize {
self.entries.lock().expect("compiled cache lock").len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
self.entries.lock().expect("compiled cache lock").clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn schema(hash: &str) -> TypeSchema {
let doc = serde_json::json!({
"schema_version": 1,
"app": "t",
"types": {"T": {"kind": "json-schema", "hash": hash, "schema": {}}},
});
super::super::SchemaSet::parse(&doc.to_string())
.expect("fixture set")
.get("T")
.expect("fixture type")
.clone()
}
#[test]
fn one_compile_per_hash_however_many_calls() {
let cache: CompiledCache<u32> = CompiledCache::new();
let s = schema("sha256:aaa");
for _ in 0..100 {
let got = cache
.get_or_compile(&s, |_| Ok::<_, ()>(7))
.expect("compiles");
assert_eq!(*got, 7);
}
assert_eq!(
cache.compilations(),
1,
"the interpreter must be built once, not per call"
);
assert_eq!(cache.len(), 1);
}
#[test]
fn a_changed_hash_recompiles() {
let cache: CompiledCache<u32> = CompiledCache::new();
let old = cache
.get_or_compile(&schema("sha256:aaa"), |_| Ok::<_, ()>(1))
.unwrap();
let new = cache
.get_or_compile(&schema("sha256:bbb"), |_| Ok::<_, ()>(2))
.unwrap();
assert_eq!((*old, *new), (1, 2));
assert_eq!(cache.compilations(), 2);
assert_eq!(cache.len(), 2);
}
#[test]
fn an_unhashed_schema_is_never_cached() {
let cache: CompiledCache<u32> = CompiledCache::new();
let mut seq = 0;
for _ in 0..3 {
seq += 1;
let got = cache
.get_or_compile(&schema(""), |_| Ok::<_, ()>(seq))
.unwrap();
assert_eq!(
*got, seq,
"each call gets its own build, never a neighbour's"
);
}
assert_eq!(cache.compilations(), 3);
assert!(cache.is_empty(), "nothing is retained under an empty key");
}
#[test]
fn a_failed_compile_leaves_nothing_behind() {
let cache: CompiledCache<u32> = CompiledCache::new();
let s = schema("sha256:aaa");
assert!(cache.get_or_compile(&s, |_| Err::<u32, _>("bad")).is_err());
assert!(cache.is_empty());
assert_eq!(*cache.get_or_compile(&s, |_| Ok::<_, ()>(9)).unwrap(), 9);
assert_eq!(cache.compilations(), 2);
}
#[test]
fn clearing_keeps_the_compile_count() {
let cache: CompiledCache<u32> = CompiledCache::new();
cache
.get_or_compile(&schema("sha256:aaa"), |_| Ok::<_, ()>(1))
.unwrap();
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.compilations(), 1);
}
}