poolcache/lib.rs
1//! A PoolCache is a hybrid LFU cache and object pool, that allows
2//! for caching behavior with the possibility of reusing object
3//! rather than dropping them from the cache automatically.
4//!
5//! # Examples
6//! ```
7//! use poolcache::PoolCache;
8//!
9//! // Create a new pool cache with a maximum 'heat' of 4.
10//! // Larger maxium heat values make popular values more resistent
11//! // to being reused, but at the cost of increasing the potential
12//! // work required to find a re-usable entry.
13//! let mut cache : PoolCache<u64, Vec<u8>> = PoolCache::new(4);
14//!
15//! // Caches are empty until you populate them..`insert` adds a
16//! // new value associated with a key.
17//! cache.insert(1, Vec::new());
18//!
19//! // `cache` now contains a single vector, associated with the key
20//! // `1`, which can be retrieved with `get`
21//! {
22//! let vecref : &Vec<u8> = cache.get(&1).unwrap();
23//! }
24//!
25//! // You can also add values that aren't associated with any key with
26//! // `put`. These newly added values will be used to satisfy `take`
27//! // requests before evicting objects with keys.
28//! cache.put(Vec::new());
29//!
30//! // You can get an owned object from the pool using `take`. This will
31//! // use any free objects (if available), or evict the least `hot`
32//! // key from the cache, and return its value.
33//! let ownedvec : Vec<u8> = cache.take().unwrap();
34//! ```
35//!
36
37use std::cell::Cell;
38use std::cmp;
39use std::collections::{BTreeMap,VecDeque};
40
41struct CacheEntry<Value> {
42 val: Value,
43 heat: Cell<u64>,
44}
45
46impl<Value> CacheEntry<Value> {
47 fn new(val: Value) -> CacheEntry<Value> {
48 CacheEntry{val: val, heat: Cell::new(1)}
49 }
50
51 fn inc(&self, max_heat: u64) -> u64 {
52 self.heat.set(cmp::min(self.heat.get() + 1, max_heat));
53 self.heat.get()
54 }
55
56 fn dec(&self) -> u64 {
57 self.heat.set(cmp::max(self.heat.get() - 1, 0));
58 self.heat.get()
59 }
60}
61
62pub struct PoolCache<Key, Value> {
63 cache: BTreeMap<Key, CacheEntry<Value>>,
64 freelist: VecDeque<Value>,
65 clock: VecDeque<Key>,
66 max_heat: u64,
67}
68
69impl<Key, Value> PoolCache<Key, Value>
70 where Key: PartialOrd + Ord + Clone {
71
72 /// Create a new PoolCache where the maximum heat of a value
73 /// is limited to `max_heat`.
74 pub fn new(max_heat: u64) -> PoolCache<Key, Value> {
75 PoolCache{
76 cache: BTreeMap::new(),
77 freelist: VecDeque::new(),
78 clock: VecDeque::new(),
79 max_heat: max_heat}
80 }
81
82 /// Returns `true` if the given key is present in the cache.
83 pub fn contains_key(&self, key: &Key) -> bool {
84 self.cache.contains_key(key)
85 }
86
87 /// Returns a reference to the value associated with `key`, or `None`
88 /// if the key is not present in the cache.
89 pub fn get(&self, key: &Key) -> Option<&Value> {
90 self.cache.get(key).and_then(|entry| {
91 entry.inc(self.max_heat);
92 Some(&entry.val)
93 })
94 }
95
96 /// Add a new object to the pool, not associated with any
97 /// key. This will become available to any callers of `take`.
98 pub fn put(&mut self, val: Value) {
99 self.freelist.push_back(val)
100 }
101
102 /// Insert `val` into the map associated with `key`. Any previous
103 /// entry for `key` will be replaced, and the old value will become
104 /// available for new callers of `take`.
105 pub fn insert(&mut self, key: Key, val: Value) {
106 let mut found_entry = false;
107 if let Some(old_entry) = self.cache.remove(&key) {
108 self.freelist.push_back(old_entry.val);
109 found_entry = true;
110 }
111 if !found_entry {
112 self.clock.push_back(key.clone());
113 }
114 self.cache.insert(key, CacheEntry::new(val));
115 }
116
117 /// Take returns an object from the pool, evicting the least-used
118 /// cached key if necessary. Returns `None` only if the PoolCache
119 /// contains no items.
120 pub fn take(&mut self) -> Option<Value> {
121 if let Some(val) = self.freelist.pop_front() {
122 return Some(val);
123 }
124 // cache is empty.
125 if self.clock.is_empty() {
126 return None;
127 }
128 // loop over the elements in `clock`, decrementing heat until
129 // we find an eligible value to evict.
130 loop {
131 let key = self.clock.pop_front().unwrap();
132 let heat = self.cache.get(&key).unwrap().dec();
133 if heat == 0 {
134 // eligible element.
135 return Some(self.cache.remove(&key).unwrap().val);
136 }
137 // non-zero heat, keep looping.
138 self.clock.push_back(key);
139 }
140 }
141}
142
143
144#[cfg(test)]
145mod test {
146 #[test]
147 fn basic() {
148 // can't take from an empty cache.
149 let mut cache: super::PoolCache<u64, String> = super::PoolCache::new(5);
150 assert_eq!(None, cache.take());
151
152 // adding an object to the cache not associated with a value,
153 // and returning it via 'take'.
154 cache.put(String::from("foo"));
155 assert_eq!(Some(String::from("foo")), cache.take());
156
157 // Since we only added one value (and then took it), the
158 // cache is empty again.
159 assert_eq!(None, cache.take());
160
161 // Add a keyed value, and retrieve it a few times.
162 cache.insert(1, String::from("bar"));
163 assert_eq!("bar", cache.get(&1).unwrap());
164 assert_eq!("bar", cache.get(&1).unwrap());
165 assert_eq!("bar", cache.get(&1).unwrap());
166
167 // Add a second value, and retrieve it only once.
168 cache.insert(2, String::from("baz"));
169 assert_eq!("baz", cache.get(&2).unwrap());
170
171
172 // taking a value returns 'baz', since it only has one use.
173 assert_eq!(Some(String::from("baz")), cache.take());
174
175 // Now that we've taken it's value, the key '2' is no longer
176 // in the cache.
177 assert_eq!(None, cache.get(&2));
178
179 // '1' is still in the cache
180 assert!(cache.contains_key(&1));
181
182 // Replace it's value (currently 'bar') with a new value.
183 cache.insert(1, String::from("newbar"));
184 assert_eq!("newbar", cache.get(&1).unwrap());
185
186 // The old value ('bar') is moved to the freelist, and is
187 // returned to the next caller of `take`
188 assert_eq!(Some(String::from("bar")), cache.take());
189
190 // A final `take` removes the last value in the pool
191 // (currently keyed to '1')
192 assert_eq!(Some(String::from("newbar")), cache.take());
193
194 // leaving the cache empty.
195 assert_eq!(None, cache.take());
196 }
197}