use crate::error::{Error, Result};
use crate::transaction::BlockRef;
use std::sync::{Mutex, PoisonError};
use std::time::{Duration, Instant};
pub const DEFAULT_MAX_AGE: Duration = Duration::from_secs(180);
#[derive(Debug, Clone, Copy)]
struct Entry {
block_ref: BlockRef,
fetched_at: Instant,
}
#[derive(Debug)]
pub struct TaposCache {
entry: Mutex<Option<Entry>>,
max_age: Duration,
}
impl TaposCache {
pub fn new() -> Self {
Self::with_max_age(DEFAULT_MAX_AGE)
}
pub fn with_max_age(max_age: Duration) -> Self {
TaposCache {
entry: Mutex::new(None),
max_age,
}
}
pub fn max_age(&self) -> Duration {
self.max_age
}
pub fn store(&self, block_ref: BlockRef) {
let mut guard = self.entry.lock().unwrap_or_else(PoisonError::into_inner);
*guard = Some(Entry {
block_ref,
fetched_at: Instant::now(),
});
}
pub fn block_ref(&self) -> Result<BlockRef> {
let guard = self.entry.lock().unwrap_or_else(PoisonError::into_inner);
let entry = guard
.as_ref()
.ok_or_else(|| Error::StaleTapos("no block reference has been fetched yet".into()))?;
let age = entry.fetched_at.elapsed();
if age > self.max_age {
return Err(Error::StaleTapos(format!(
"cached block reference is {}s old, limit is {}s",
age.as_secs(),
self.max_age.as_secs()
)));
}
Ok(entry.block_ref)
}
pub fn age(&self) -> Option<Duration> {
let guard = self.entry.lock().unwrap_or_else(PoisonError::into_inner);
guard.as_ref().map(|e| e.fetched_at.elapsed())
}
pub fn is_fresh(&self) -> bool {
self.block_ref().is_ok()
}
pub fn invalidate(&self) {
let mut guard = self.entry.lock().unwrap_or_else(PoisonError::into_inner);
*guard = None;
}
}
impl Default for TaposCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn a_ref() -> BlockRef {
BlockRef {
ref_block_num: 1,
ref_block_prefix: 2,
block_num: 3,
}
}
#[test]
fn an_empty_cache_refuses_rather_than_guessing() {
let cache = TaposCache::new();
assert!(!cache.is_fresh());
assert!(matches!(cache.block_ref(), Err(Error::StaleTapos(_))));
assert!(cache.age().is_none());
}
#[test]
fn a_stored_reference_is_served() {
let cache = TaposCache::new();
cache.store(a_ref());
assert_eq!(cache.block_ref().unwrap(), a_ref());
assert!(cache.is_fresh());
assert!(cache.age().unwrap() < Duration::from_secs(1));
}
#[test]
fn a_stale_reference_is_refused_not_served() {
let cache = TaposCache::with_max_age(Duration::from_nanos(1));
cache.store(a_ref());
std::thread::sleep(Duration::from_millis(2));
match cache.block_ref() {
Err(Error::StaleTapos(msg)) => assert!(msg.contains("old")),
other => panic!("expected a staleness refusal, got {other:?}"),
}
assert!(!cache.is_fresh());
}
#[test]
fn invalidate_forces_a_refresh() {
let cache = TaposCache::new();
cache.store(a_ref());
assert!(cache.is_fresh());
cache.invalidate();
assert!(!cache.is_fresh());
}
#[test]
fn is_shareable_across_threads() {
use std::sync::Arc;
let cache = Arc::new(TaposCache::new());
cache.store(a_ref());
let handles: Vec<_> = (0..8)
.map(|_| {
let cache = Arc::clone(&cache);
std::thread::spawn(move || cache.block_ref().unwrap())
})
.collect();
for h in handles {
assert_eq!(h.join().unwrap(), a_ref());
}
}
}