use super::{error::CacheError, Cache};
use pdk_core::classy::{Clock, TimeUnit};
use std::rc::Rc;
use std::time::Duration;
#[derive(serde::Serialize, serde::Deserialize)]
struct TtlEntry {
expiry_ms: u128,
value: Vec<u8>,
}
pub(crate) struct TtlCache<C: Cache> {
inner: C,
clock: Rc<dyn Clock>,
ttl_ms: u128,
}
impl<C: Cache> TtlCache<C> {
pub(crate) fn new(inner: C, clock: Rc<dyn Clock>, ttl: Duration) -> Self {
Self {
inner,
clock,
ttl_ms: ttl.as_millis(),
}
}
fn now_ms(&self) -> u128 {
self.clock.get_current_time_unit(TimeUnit::Milliseconds)
}
}
impl<C: Cache> Cache for TtlCache<C> {
fn save(&self, key: &str, value: Vec<u8>) -> Result<(), CacheError> {
let expiry_ms = self.now_ms().saturating_add(self.ttl_ms);
let entry = TtlEntry { expiry_ms, value };
let serialized = serde_fixint::to_vec(&entry).map_err(|e| CacheError::SaveError {
status: format!("Failed to serialize TTL entry: {e}"),
cache_id: key.to_string(),
})?;
self.inner.save(key, serialized)
}
fn get(&self, key: &str) -> Option<Vec<u8>> {
let raw = self.inner.get(key)?;
let entry: TtlEntry = serde_fixint::from_slice(&raw).ok()?;
if self.now_ms() >= entry.expiry_ms {
self.inner.delete(key);
None
} else {
Some(entry.value)
}
}
fn delete(&self, key: &str) -> Option<Vec<u8>> {
let raw = self.inner.delete(key)?;
serde_fixint::from_slice::<TtlEntry>(&raw)
.ok()
.map(|e| e.value)
}
fn purge(&self) {
self.inner.purge()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::time::{Duration, SystemTime};
struct ManualClock {
current: RefCell<SystemTime>,
}
impl ManualClock {
fn new() -> Self {
Self {
current: RefCell::new(SystemTime::now()),
}
}
fn move_forward(&self, duration: Duration) {
let new_time = self.get_current_time() + duration;
*self.current.borrow_mut() = new_time;
}
}
impl Clock for ManualClock {
fn get_current_time(&self) -> SystemTime {
*self.current.borrow()
}
}
struct FakeCache {
store: RefCell<HashMap<String, Vec<u8>>>,
delete_calls: RefCell<Vec<String>>,
}
impl FakeCache {
fn new() -> Self {
Self {
store: RefCell::new(HashMap::new()),
delete_calls: RefCell::new(Vec::new()),
}
}
fn delete_was_called(&self, key: &str) -> bool {
self.delete_calls.borrow().contains(&key.to_string())
}
}
impl Cache for FakeCache {
fn save(&self, key: &str, value: Vec<u8>) -> Result<(), CacheError> {
self.store.borrow_mut().insert(key.to_string(), value);
Ok(())
}
fn get(&self, key: &str) -> Option<Vec<u8>> {
self.store.borrow().get(key).cloned()
}
fn delete(&self, key: &str) -> Option<Vec<u8>> {
self.delete_calls.borrow_mut().push(key.to_string());
self.store.borrow_mut().remove(key)
}
fn purge(&self) {
self.store.borrow_mut().clear();
}
}
#[test]
fn get_returns_value_before_ttl() {
let manual_clock = Rc::new(ManualClock::new());
let clock: Rc<dyn Clock> = Rc::clone(&manual_clock) as Rc<dyn Clock>;
let inner = FakeCache::new();
let ttl_cache = TtlCache::new(inner, Rc::clone(&clock), Duration::from_secs(10));
ttl_cache.save("key1", b"value1".to_vec()).unwrap();
manual_clock.move_forward(Duration::from_secs(5));
assert_eq!(ttl_cache.get("key1"), Some(b"value1".to_vec()));
}
#[test]
fn get_returns_none_after_ttl() {
let manual_clock = Rc::new(ManualClock::new());
let clock: Rc<dyn Clock> = Rc::clone(&manual_clock) as Rc<dyn Clock>;
let inner = FakeCache::new();
let ttl_cache = TtlCache::new(inner, Rc::clone(&clock), Duration::from_secs(10));
ttl_cache.save("key1", b"value1".to_vec()).unwrap();
manual_clock.move_forward(Duration::from_secs(11));
assert!(ttl_cache.get("key1").is_none());
}
#[test]
fn get_deletes_expired_entry_from_inner_cache() {
let manual_clock = Rc::new(ManualClock::new());
let clock: Rc<dyn Clock> = Rc::clone(&manual_clock) as Rc<dyn Clock>;
let inner = FakeCache::new();
let ttl_cache = TtlCache::new(inner, Rc::clone(&clock), Duration::from_secs(10));
ttl_cache.save("key1", b"value1".to_vec()).unwrap();
manual_clock.move_forward(Duration::from_secs(11));
assert!(ttl_cache.get("key1").is_none());
assert!(ttl_cache.inner.delete_was_called("key1"));
}
#[test]
fn get_returns_none_at_exact_expiry_boundary() {
let manual_clock = Rc::new(ManualClock::new());
let clock: Rc<dyn Clock> = Rc::clone(&manual_clock) as Rc<dyn Clock>;
let inner = FakeCache::new();
let ttl_cache = TtlCache::new(inner, Rc::clone(&clock), Duration::from_secs(10));
ttl_cache.save("key1", b"value1".to_vec()).unwrap();
manual_clock.move_forward(Duration::from_secs(10));
assert!(ttl_cache.get("key1").is_none());
}
#[test]
fn roundtrip_preserves_value_integrity() {
let clock: Rc<dyn Clock> = Rc::new(ManualClock::new());
let inner = FakeCache::new();
let ttl_cache = TtlCache::new(inner, Rc::clone(&clock), Duration::from_secs(10));
let original = b"test value with special \x00 bytes";
ttl_cache.save("key1", original.to_vec()).unwrap();
assert_eq!(ttl_cache.get("key1"), Some(original.to_vec()));
}
#[test]
fn delete_returns_unprefixed_value() {
let clock: Rc<dyn Clock> = Rc::new(ManualClock::new());
let inner = FakeCache::new();
let ttl_cache = TtlCache::new(inner, Rc::clone(&clock), Duration::from_secs(10));
ttl_cache.save("key1", b"value1".to_vec()).unwrap();
assert_eq!(ttl_cache.delete("key1"), Some(b"value1".to_vec()));
}
#[test]
fn get_returns_none_for_malformed_bytes_without_panic() {
let clock: Rc<dyn Clock> = Rc::new(ManualClock::new());
let inner = FakeCache::new();
inner.save("key1", vec![1, 2, 3]).unwrap();
let ttl_cache = TtlCache::new(inner, Rc::clone(&clock), Duration::from_secs(10));
assert!(ttl_cache.get("key1").is_none());
}
#[test]
fn delete_returns_none_for_malformed_bytes() {
let clock: Rc<dyn Clock> = Rc::new(ManualClock::new());
let inner = FakeCache::new();
inner.save("key1", vec![1, 2, 3]).unwrap();
let ttl_cache = TtlCache::new(inner, Rc::clone(&clock), Duration::from_secs(10));
assert!(ttl_cache.delete("key1").is_none());
}
#[test]
fn purge_delegates_to_inner() {
let clock: Rc<dyn Clock> = Rc::new(ManualClock::new());
let inner = FakeCache::new();
let ttl_cache = TtlCache::new(inner, Rc::clone(&clock), Duration::from_secs(10));
ttl_cache.save("key1", b"value1".to_vec()).unwrap();
ttl_cache.save("key2", b"value2".to_vec()).unwrap();
ttl_cache.purge();
assert!(ttl_cache.get("key1").is_none());
assert!(ttl_cache.get("key2").is_none());
}
use crate::lru_cache::LruCache;
use data_storage_lib::{BlockingDataStorage, DataStorageError, StoreMode};
use serde::{de::DeserializeOwned, Serialize};
#[derive(Default)]
struct FakeStore {
entries: RefCell<HashMap<String, (Vec<u8>, u32)>>,
cas_counter: RefCell<u32>,
}
impl FakeStore {
fn next_cas(&self) -> u32 {
let mut counter = self.cas_counter.borrow_mut();
*counter += 1;
*counter
}
}
impl BlockingDataStorage for FakeStore {
fn get_keys(&self) -> Result<Vec<String>, DataStorageError> {
Ok(self.entries.borrow().keys().cloned().collect())
}
fn store<T: Serialize>(
&self,
key: &str,
mode: &StoreMode,
item: &T,
) -> Result<(), DataStorageError> {
let serialized = serde_fixint::to_vec(item)?;
let mut entries = self.entries.borrow_mut();
match mode {
StoreMode::Always => {}
StoreMode::Absent => {
if entries.contains_key(key) {
return Err(DataStorageError::CasMismatch);
}
}
StoreMode::Cas(expected) => {
let expected: u32 = expected.parse()?;
match entries.get(key) {
Some((_, cas)) if *cas == expected => {}
_ => return Err(DataStorageError::CasMismatch),
}
}
}
let cas = self.next_cas();
entries.insert(key.to_string(), (serialized, cas));
Ok(())
}
fn get<T: DeserializeOwned>(
&self,
key: &str,
) -> Result<Option<(T, String)>, DataStorageError> {
match self.entries.borrow().get(key) {
Some((data, cas)) => {
let deserialized: T = serde_fixint::from_slice(data)?;
Ok(Some((deserialized, cas.to_string())))
}
None => Ok(None),
}
}
fn delete(&self, key: &str) -> Result<(), DataStorageError> {
self.entries.borrow_mut().remove(key);
Ok(())
}
fn delete_all(&self) -> Result<(), DataStorageError> {
self.entries.borrow_mut().clear();
Ok(())
}
}
#[test]
fn ttl_wrapping_lru_cache_present_before_ttl() {
let manual_clock = Rc::new(ManualClock::new());
let clock: Rc<dyn Clock> = Rc::clone(&manual_clock) as Rc<dyn Clock>;
let storage = FakeStore::default();
let lru = LruCache::new(String::from("test-ttl-lru"), 100, storage, 16);
let ttl_cache = TtlCache::new(lru, Rc::clone(&clock), Duration::from_secs(10));
ttl_cache.save("key1", b"value1".to_vec()).unwrap();
manual_clock.move_forward(Duration::from_secs(5));
assert_eq!(ttl_cache.get("key1"), Some(b"value1".to_vec()));
}
#[test]
fn ttl_wrapping_lru_cache_gone_after_ttl() {
let manual_clock = Rc::new(ManualClock::new());
let clock: Rc<dyn Clock> = Rc::clone(&manual_clock) as Rc<dyn Clock>;
let storage = FakeStore::default();
let lru = LruCache::new(String::from("test-ttl-lru"), 100, storage, 16);
let ttl_cache = TtlCache::new(lru, Rc::clone(&clock), Duration::from_secs(10));
ttl_cache.save("key1", b"value1".to_vec()).unwrap();
manual_clock.move_forward(Duration::from_secs(11));
assert!(ttl_cache.get("key1").is_none());
}
}