fastcache/
lib.rs

1#![cfg_attr(not(doctest), doc = include_str!("../README.md"))]
2
3use std::{
4    hash::Hash,
5    sync::atomic::{AtomicBool, Ordering},
6    time::{Duration, Instant},
7};
8
9use crossbeam_queue::ArrayQueue;
10use crossbeam_utils::{atomic::AtomicCell, CachePadded};
11use dashmap::DashMap;
12
13/// Represents an entry in the cache.
14///
15/// Wraps a value with an expiration timestamp and an expired flag.
16pub struct Value<V> {
17    value: V,
18    expire_at: Instant,
19    is_expired: bool,
20}
21
22impl<V> Value<V> {
23    /// Get a reference to the inner value.
24    pub fn get(&self) -> &V {
25        &self.value
26    }
27
28    /// Get a mutable reference to the inner value.
29    pub fn get_mut(&mut self) -> &mut V {
30        &mut self.value
31    }
32
33    /// Consumes the `Value` and returns its inner value.
34    pub fn into_inner(self) -> V {
35        self.value
36    }
37
38    /// Check if the value is expired.
39    pub fn is_expired(&self) -> bool {
40        self.is_expired
41    }
42
43    /// Get the expiration timestamp of the value.
44    pub fn expire_at(&self) -> Instant {
45        self.expire_at
46    }
47}
48
49impl<V> std::ops::Deref for Value<V> {
50    type Target = V;
51
52    fn deref(&self) -> &Self::Target {
53        &self.value
54    }
55}
56
57impl<V> std::ops::DerefMut for Value<V> {
58    fn deref_mut(&mut self) -> &mut Self::Target {
59        &mut self.value
60    }
61}
62
63/// A not so accurate but performant time and capacity based cache.
64pub struct Cache<K, V> {
65    map: DashMap<K, (V, Instant), ahash::RandomState>,
66    ringbuf: ArrayQueue<(K, Instant)>,
67
68    capacity: usize,
69    ttl: Duration,
70
71    expire_started: CachePadded<AtomicBool>,
72    oldest: CachePadded<AtomicCell<Instant>>,
73}
74
75impl<K, V> Cache<K, V>
76where
77    K: Eq + Hash + Clone,
78    V: Clone,
79{
80    /// Create a new cache with the given capacity and time-to-live (TTL) for values.
81    pub fn new(capacity: usize, ttl: Duration) -> Self {
82        Self {
83            map: DashMap::with_capacity_and_hasher(capacity, ahash::RandomState::new()),
84            ringbuf: ArrayQueue::new(capacity),
85            capacity,
86            ttl,
87            expire_started: CachePadded::new(AtomicBool::new(false)),
88            oldest: CachePadded::new(AtomicCell::new(Instant::now())),
89        }
90    }
91
92    /// Get the number of elements in the cache.
93    pub fn len(&self) -> usize {
94        self.ringbuf.len()
95    }
96
97    /// Get the capacity of the cache.
98    pub fn capacity(&self) -> usize {
99        self.capacity
100    }
101
102    /// Get the value associated with the given key, if it exists and is not expired.
103    pub fn get(&self, key: &K) -> Option<Value<V>> {
104        let v = self.map.get(&key);
105        if v.is_none() {
106            return None;
107        }
108        let v = v.unwrap();
109        let now = Instant::now();
110        let value = Value {
111            value: v.0.clone(),
112            expire_at: v.1,
113            is_expired: now > v.1,
114        };
115        // The reference must be dropped, or will cause a deadlock when do expire.
116        drop(v);
117        self.do_expire(now);
118        Some(value)
119    }
120
121    /// Insert a key-value pair in the cache.
122    ///
123    /// If the cache is full, it will evict the oldest entry.
124    pub fn insert(&self, key: K, value: V) {
125        let now = Instant::now();
126        let expire_at = now + self.ttl;
127        while let Err(_) = self.ringbuf.push((key.clone(), expire_at)) {
128            // ringbuf is full, pop one
129            let (k, e) = self.ringbuf.pop().unwrap();
130            self.map.remove(&k);
131            self.oldest.store(e);
132        }
133        self.map.insert(key, (value, expire_at));
134        self.do_expire(now);
135    }
136
137    pub fn remove(&self, key: &K) -> Option<V> {
138        self.map.remove(key).map(|v| v.1.0)
139    }
140
141    /// Check and evict expired items in the cache.
142    fn do_expire(&self, now: Instant) {
143        if self.oldest.load() > now {
144            // don't need to do expire
145            return;
146        }
147
148        // grab the lock, a simple singleflight implementation
149        if self
150            .expire_started
151            .compare_exchange_weak(false, true, Ordering::AcqRel, Ordering::Relaxed)
152            .is_err()
153        {
154            return;
155        }
156
157        while let Some((k, t)) = self.ringbuf.pop() {
158            self.map.remove(&k);
159            if now <= t {
160                // TODO: find a way to put it back, or peek it instead of pop.
161                self.oldest.store(t);
162                break;
163            }
164        }
165        self.expire_started.store(false, Ordering::Release);
166    }
167
168    pub fn clear(&self) {
169        while let Some((k, _)) = self.ringbuf.pop() {
170            self.map.remove(&k);
171        }
172        self.oldest.store(Instant::now());
173    }
174}
175
176/// A capacity based fifo cache.
177pub struct SizedCache<K, V> {
178    map: DashMap<K, V, ahash::RandomState>,
179    ringbuf: ArrayQueue<K>,
180
181    capacity: usize,
182}
183
184impl<K, V> SizedCache<K, V>
185where
186    K: Eq + Hash + Clone,
187    V: Clone,
188{
189    /// Create a new cache with the given capacity.
190    pub fn new(capacity: usize) -> Self {
191        Self {
192            map: DashMap::with_capacity_and_hasher(capacity, ahash::RandomState::new()),
193            ringbuf: ArrayQueue::new(capacity),
194            capacity,
195        }
196    }
197
198    /// Get the number of elements in the cache.
199    pub fn len(&self) -> usize {
200        self.ringbuf.len()
201    }
202
203    /// Get the capacity of the cache.
204    pub fn capacity(&self) -> usize {
205        self.capacity
206    }
207
208    /// Get the value associated with the given key.
209    pub fn get(&self, key: &K) -> Option<V> {
210        self.map.get(&key).map(|v| v.value().clone())
211    }
212
213    /// Insert a key-value pair in the cache.
214    ///
215    /// If the cache is full, it will evict the oldest entry.
216    pub fn insert(&self, key: K, value: V) {
217        while let Err(_) = self.ringbuf.push(key.clone()) {
218            // ringbuf is full, pop one
219            let k = self.ringbuf.pop().unwrap();
220            self.map.remove(&k);
221        }
222        self.map.insert(key, value);
223    }
224
225    pub fn remove(&self, key: &K) -> Option<V> {
226        self.map.remove(key).map(|v| v.1)
227    }
228
229    pub fn clear(&self) {
230        while let Some(k) = self.ringbuf.pop() {
231            self.map.remove(&k);
232        }
233    }
234}