use std::sync::{Arc, Mutex};
use writium::prelude::*;
use item::CacheItem;
const ERR_POISONED_THREAD: &str = "Current thread is poisoned.";
pub struct Cache<T: 'static> {
cache: Mutex<Vec<Arc<CacheItem<T>>>>,
src: Box<CacheSource<Value=T>>,
}
impl<T: 'static> Cache<T> {
pub fn new<Src>(capacity: usize, src: Src) -> Cache<T>
where Src: 'static + CacheSource<Value=T> {
Cache {
cache: Mutex::new(Vec::with_capacity(capacity)),
src: Box::new(src),
}
}
pub fn create(&self, id: &str) -> Result<Arc<CacheItem<T>>> {
self._get(&id, true)
}
pub fn get(&self, id: &str) -> Result<Arc<CacheItem<T>>> {
self._get(&id, false)
}
fn _get(&self, id: &str, create: bool) -> Result<Arc<CacheItem<T>>> {
let mut cache = self.cache.lock()
.map_err(|_| Error::internal(ERR_POISONED_THREAD))?;
if let Some(pos) = cache.iter()
.position(|item| item.id() == id) {
let arc = cache.remove(pos);
cache.insert(0, arc.clone());
return Ok(arc)
} else {
let new_item = CacheItem::new(id, self.src.load(id, create)?);
let new_arc = Arc::new(new_item);
if cache.capacity() == 0 {
return Ok(new_arc)
}
if cache.len() == cache.capacity() {
let lru_item = cache.pop().unwrap();
if lru_item.is_dirty() {
let data = lru_item.write()?;
if let Err(err) = self.src.unload(lru_item.id(), &*data) {
error!("Unable to unload '{}': {}", id, err);
}
}
}
cache.insert(0, new_arc.clone());
return Ok(new_arc)
}
}
pub fn remove(&self, id: &str) -> Result<()> {
let mut cache = self.cache.lock().unwrap();
cache.iter()
.position(|nid| nid.id() == id)
.map(|pos| cache.remove(pos));
self.src.remove(&id)
}
#[cfg(test)]
pub fn capacity(&self) -> usize {
self.cache.lock().unwrap().capacity()
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.cache.lock().unwrap().len()
}
}
pub trait CacheSource: 'static + Send + Sync {
type Value: 'static;
fn load(&self, id: &str, create: bool) -> Result<Self::Value>;
fn unload(&self, _id: &str, _obj: &Self::Value) -> Result<()> {
Ok(())
}
fn remove(&self, _id: &str) -> Result<()> {
Ok(())
}
}
impl<T: 'static> Drop for Cache<T> {
fn drop(&mut self) {
let mut lock = self.cache.lock().unwrap();
while let Some(item) = lock.pop() {
if !item.is_dirty() { continue }
let guard = item.write().unwrap();
if let Err(err) = self.src.unload(item.id(), &guard) {
warn!("Unable to unload '{}': {}", item.id(), err);
}
}
}
}
#[cfg(test)]
mod tests {
use writium::prelude::*;
struct TestSource(bool);
impl super::CacheSource for TestSource {
type Value = &'static str;
fn load(&self, id: &str, _create: bool) -> Result<Self::Value> {
if self.0 { Err(Error::not_found("")) }
else { Ok(&["cache0", "cache1", "cache2", "cache3"][id.parse::<usize>().unwrap()]) }
}
fn unload(&self, _id: &str, _obj: &Self::Value) -> Result<()> {
Ok(())
}
fn remove(&self, _id: &str) -> Result<()> {
Ok(())
}
}
type TestCache = super::Cache<&'static str>;
fn make_cache(fail: bool) -> TestCache {
TestCache::new(3, TestSource(fail))
}
#[test]
fn test_cache() {
let cache = make_cache(false);
assert!(cache.get("0").is_ok());
assert!(cache.get("1").is_ok());
assert!(cache.get("2").is_ok());
}
#[test]
fn test_cache_failure() {
let cache = make_cache(true);
assert!(cache.get("0").is_err());
assert!(cache.get("1").is_err());
assert!(cache.get("2").is_err());
}
#[test]
fn test_max_cache() {
let cache = make_cache(false);
assert!(cache.len() == 0);
assert!(cache.get("0").is_ok());
assert!(cache.len() == 1);
assert!(cache.get("1").is_ok());
assert!(cache.len() == 2);
assert!(cache.get("2").is_ok());
assert!(cache.len() == 3);
assert!(cache.get("3").is_ok());
assert!(cache.len() == 3);
}
#[test]
fn test_max_cache_failure() {
let cache = make_cache(true);
assert!(cache.len() == 0);
assert!(cache.get("0").is_err());
assert!(cache.len() == 0);
assert!(cache.get("1").is_err());
assert!(cache.len() == 0);
assert!(cache.get("2").is_err());
assert!(cache.len() == 0);
}
#[test]
fn test_remove() {
let cache = make_cache(false);
assert!(cache.get("0").is_ok());
assert!(cache.len() == 1);
assert!(cache.remove("0").is_ok());
assert!(cache.len() == 0);
assert!(cache.remove("0").is_ok());
}
}