//! This implements a time-based LRU cache for fast checking of duplicates use fnv::FnvHashSet; #[cfg(test)] use mock_instant::Instant; use std::collections::VecDeque; #[cfg(not(test))] use std::time::Instant; use std::time::Duration; struct Element { /// The key being inserted. key: Key, /// The instant the key was inserted. inserted: Instant, } pub struct LRUTimeCache { /// The duplicate cache. map: FnvHashSet, /// An ordered list of keys by insert time. list: VecDeque>, /// The time elements remain in the cache. ttl: Duration, } impl LRUTimeCache where Key: Eq + std::hash::Hash + Clone, { pub fn new(ttl: Duration) -> Self { LRUTimeCache { map: FnvHashSet::default(), list: VecDeque::new(), ttl, } } /// Inserts a key without removal of potentially expired elements. /// Returns true if the key does not already exist. pub fn raw_insert(&mut self, key: Key) -> bool { // check the cache before removing elements let is_new = self.map.insert(key.clone()); // add the new key to the list, if it doesn't already exist. if is_new { self.list.push_back(Element { key, inserted: Instant::now(), }); } else { let position = self .list .iter() .position(|e| e.key == key) .expect("Key is not new"); let mut element = self .list .remove(position) .expect("Position is not occupied"); element.inserted = Instant::now(); self.list.push_back(element); } #[cfg(test)] self.check_invariant(); is_new } /// Removes a key from the cache without purging expired elements. Returns true if the key /// existed. pub fn raw_remove(&mut self, key: &Key) -> bool { if self.map.remove(key) { let position = self .list .iter() .position(|e| &e.key == key) .expect("Key must exist"); self.list .remove(position) .expect("Position is not occupied"); true } else { false } } /// Removes all expired elements and returns them pub fn remove_expired(&mut self) -> Vec { if self.list.is_empty() { return Vec::new(); } let mut removed_elements = Vec::new(); let now = Instant::now(); // remove any expired results while let Some(element) = self.list.pop_front() { if element.inserted + self.ttl > now { self.list.push_front(element); break; } self.map.remove(&element.key); removed_elements.push(element.key); } #[cfg(test)] self.check_invariant(); removed_elements } // Inserts a new key. It first purges expired elements to do so. // // If the key was not present this returns `true`. If the value was already present this // returns `false` and updates the insertion time of the key. pub fn insert(&mut self, key: Key) -> bool { self.update(); // check the cache before removing elements let is_new = self.map.insert(key.clone()); // add the new key to the list, if it doesn't already exist. if is_new { self.list.push_back(Element { key, inserted: Instant::now(), }); } else { let position = self .list .iter() .position(|e| e.key == key) .expect("Key is not new"); let mut element = self .list .remove(position) .expect("Position is not occupied"); element.inserted = Instant::now(); self.list.push_back(element); } #[cfg(test)] self.check_invariant(); is_new } /// Removes any expired elements from the cache. pub fn update(&mut self) { if self.list.is_empty() { return; } let now = Instant::now(); // remove any expired results while let Some(element) = self.list.pop_front() { if element.inserted + self.ttl > now { self.list.push_front(element); break; } self.map.remove(&element.key); } #[cfg(test)] self.check_invariant() } /// Returns if the key is present after removing expired elements. pub fn contains(&mut self, key: &Key) -> bool { self.update(); self.map.contains(key) } /// List known keys pub fn keys(&mut self) -> impl Iterator { self.update(); self.map.iter() } /// Shrink the mappings to fit the current size. pub fn shrink_to_fit(&mut self) { self.map.shrink_to_fit(); self.list.shrink_to_fit(); } #[cfg(test)] #[track_caller] fn check_invariant(&self) { // The list should be sorted. First element should have the oldest insertion let mut prev_insertion_time = None; for e in &self.list { match prev_insertion_time { Some(prev) => { if prev <= e.inserted { prev_insertion_time = Some(e.inserted); } else { panic!("List is not sorted by insertion time") } } None => prev_insertion_time = Some(e.inserted), } // The key should be in the map assert!(self.map.contains(&e.key), "List and map should be in sync"); } for k in &self.map { let _ = self .list .iter() .position(|e| &e.key == k) .expect("Map and list should be in sync"); } // One last check to make sure there are no duplicates in the list assert_eq!(self.list.len(), self.map.len()); } } #[cfg(test)] mod test { use super::*; #[test] fn cache_added_entries_exist() { let mut cache = LRUTimeCache::new(Duration::from_secs(10)); cache.insert("t"); cache.insert("e"); // Should report that 't' and 't' already exists assert!(!cache.insert("t")); assert!(!cache.insert("e")); } #[test] fn test_reinsertion_updates_timeout() { let mut cache = LRUTimeCache::new(Duration::from_millis(100)); cache.insert("a"); cache.insert("b"); mock_instant::MockClock::advance(Duration::from_millis(20)); cache.insert("a"); // a is newer now mock_instant::MockClock::advance(Duration::from_millis(85)); assert!(cache.contains(&"a"),); // b was inserted first but was not as recent it should have been removed assert!(!cache.contains(&"b")); mock_instant::MockClock::advance(Duration::from_millis(16)); assert!(!cache.contains(&"a")); } }