use libdictenstein::{
Dictionary, DictionaryNode, DictionaryValue, MappedDictionary, MappedDictionaryNode,
SyncStrategy,
};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
struct EntryMetadata {
inserted_at: Instant,
}
impl EntryMetadata {
fn new() -> Self {
Self {
inserted_at: Instant::now(),
}
}
fn age(&self) -> Duration {
self.inserted_at.elapsed()
}
fn is_expired(&self, ttl: Duration) -> bool {
self.age() > ttl
}
}
#[derive(Clone)]
pub struct Ttl<D> {
inner: D,
ttl: Duration,
metadata: Arc<RwLock<HashMap<String, EntryMetadata>>>,
}
impl<D> Ttl<D> {
pub fn new(dict: D, ttl: Duration) -> Self {
Self {
inner: dict,
ttl,
metadata: Arc::new(RwLock::new(HashMap::new())),
}
}
#[inline]
pub fn into_inner(self) -> D {
self.inner
}
#[inline]
pub fn inner(&self) -> &D {
&self.inner
}
#[inline]
pub fn ttl(&self) -> Duration {
self.ttl
}
fn is_expired(&self, term: &str) -> bool {
let metadata = self
.metadata
.read()
.expect("poisoned RwLock; only fatal if writer panicked");
if let Some(entry_meta) = metadata.get(term) {
entry_meta.is_expired(self.ttl)
} else {
false
}
}
fn record_access(&self, term: &str) {
let mut metadata = self
.metadata
.write()
.expect("poisoned RwLock; only fatal if writer panicked");
metadata
.entry(term.to_string())
.or_insert_with(EntryMetadata::new);
}
pub fn cleanup_expired(&self) {
let mut metadata = self
.metadata
.write()
.expect("poisoned RwLock; only fatal if writer panicked");
metadata.retain(|_, entry_meta| !entry_meta.is_expired(self.ttl));
}
}
impl<D> Dictionary for Ttl<D>
where
D: Dictionary,
{
type Node = TtlNode<D::Node>;
#[inline]
fn root(&self) -> Self::Node {
TtlNode::new(self.inner.root(), self.ttl, Arc::clone(&self.metadata))
}
#[inline]
fn len(&self) -> Option<usize> {
self.inner.len()
}
#[inline]
fn contains(&self, term: &str) -> bool {
if self.is_expired(term) {
return false;
}
self.inner.contains(term)
}
#[inline]
fn sync_strategy(&self) -> SyncStrategy {
self.inner.sync_strategy()
}
}
impl<D, V> MappedDictionary for Ttl<D>
where
D: MappedDictionary<Value = V>,
V: DictionaryValue,
{
type Value = V;
#[inline]
fn get_value(&self, term: &str) -> Option<Self::Value> {
if self.is_expired(term) {
return None;
}
self.record_access(term);
self.inner.get_value(term)
}
#[inline]
fn contains_with_value<F>(&self, term: &str, predicate: F) -> bool
where
F: Fn(&Self::Value) -> bool,
{
if self.is_expired(term) {
return false;
}
self.inner.contains_with_value(term, predicate)
}
}
#[derive(Clone)]
pub struct TtlNode<N> {
inner: N,
ttl: Duration,
metadata: Arc<RwLock<HashMap<String, EntryMetadata>>>,
}
impl<N> TtlNode<N> {
fn new(inner: N, ttl: Duration, metadata: Arc<RwLock<HashMap<String, EntryMetadata>>>) -> Self {
Self {
inner,
ttl,
metadata,
}
}
}
impl<N> DictionaryNode for TtlNode<N>
where
N: DictionaryNode,
{
type Unit = N::Unit;
#[inline]
fn is_final(&self) -> bool {
self.inner.is_final()
}
#[inline]
fn transition(&self, label: Self::Unit) -> Option<Self> {
self.inner
.transition(label)
.map(|node| TtlNode::new(node, self.ttl, Arc::clone(&self.metadata)))
}
#[inline]
fn edges(&self) -> Box<dyn Iterator<Item = (Self::Unit, Self)> + '_> {
let ttl = self.ttl;
let metadata = Arc::clone(&self.metadata);
Box::new(
self.inner
.edges()
.map(move |(label, node)| (label, TtlNode::new(node, ttl, Arc::clone(&metadata)))),
)
}
#[inline]
fn edge_count(&self) -> Option<usize> {
self.inner.edge_count()
}
}
impl<N, V> MappedDictionaryNode for TtlNode<N>
where
N: MappedDictionaryNode<Value = V>,
V: DictionaryValue,
{
type Value = V;
#[inline]
fn value(&self) -> Option<Self::Value> {
self.inner.value()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "pathmap-backend")]
use libdictenstein::pathmap::PathMapDictionary;
use std::thread;
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_ttl_wrapper_basic() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42), ("bar", 99)]);
let ttl = Ttl::new(dict, Duration::from_secs(300));
assert_eq!(ttl.get_value("foo"), Some(42));
assert_eq!(ttl.get_value("bar"), Some(99));
assert!(ttl.contains("foo"));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_ttl_expiration() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42), ("bar", 99)]);
let ttl = Ttl::new(dict, Duration::from_millis(50));
assert_eq!(ttl.get_value("foo"), Some(42));
thread::sleep(Duration::from_millis(60));
assert_eq!(ttl.get_value("foo"), None);
assert!(!ttl.contains("foo"));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_ttl_unaccessed_entries() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42), ("bar", 99)]);
let ttl = Ttl::new(dict, Duration::from_millis(50));
thread::sleep(Duration::from_millis(60));
assert_eq!(ttl.get_value("bar"), Some(99));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_ttl_cleanup() {
let dict =
PathMapDictionary::from_terms_with_values([("foo", 42), ("bar", 99), ("baz", 123)]);
let ttl = Ttl::new(dict, Duration::from_millis(50));
assert_eq!(ttl.get_value("foo"), Some(42));
assert_eq!(ttl.get_value("bar"), Some(99));
assert_eq!(ttl.get_value("baz"), Some(123));
thread::sleep(Duration::from_millis(60));
assert_eq!(ttl.get_value("foo"), None);
assert_eq!(ttl.get_value("bar"), None);
assert_eq!(ttl.get_value("baz"), None);
ttl.cleanup_expired();
let metadata = ttl
.metadata
.read()
.expect("poisoned RwLock; only fatal if writer panicked");
assert_eq!(metadata.len(), 0);
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_ttl_node_traversal() {
let dict = PathMapDictionary::<()>::from_terms(["hello", "help"]);
let ttl = Ttl::new(dict, Duration::from_secs(300));
let root = ttl.root();
assert!(!root.is_final());
let h = root
.transition(b'h')
.expect("expected Some transition h in test");
let e = h
.transition(b'e')
.expect("expected Some transition e in test");
let l = e
.transition(b'l')
.expect("expected Some transition l in test");
let p = l
.transition(b'p')
.expect("expected Some transition p in test");
assert!(p.is_final()); }
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_ttl_contains_with_value() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42), ("bar", 99)]);
let ttl = Ttl::new(dict, Duration::from_millis(50));
assert_eq!(ttl.get_value("foo"), Some(42));
assert!(ttl.contains_with_value("foo", |v| *v == 42));
thread::sleep(Duration::from_millis(60));
assert!(!ttl.contains_with_value("foo", |v| *v == 42));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_ttl_into_inner() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42)]);
let ttl = Ttl::new(dict, Duration::from_secs(300));
let original = ttl.into_inner();
assert_eq!(original.len(), Some(1));
assert_eq!(original.get_value("foo"), Some(42));
}
}