1use core::hash::Hash;
4use std::collections::HashMap;
5use std::num::NonZeroUsize;
6use std::time::{Duration, Instant};
7
8use crate::cache::Cache;
9use crate::error::CacheError;
10use crate::sharding::{self, Sharded};
11use crate::util::MutexExt;
12
13const FAR_FUTURE: Duration = Duration::from_secs(60 * 60 * 24 * 365 * 100);
16
17pub struct TtlCache<K, V> {
52 capacity: NonZeroUsize,
53 default_ttl: Duration,
54 sharded: Sharded<Inner<K, V>>,
55}
56
57struct Entry<V> {
58 value: V,
59 expires_at: Instant,
60}
61
62struct Inner<K, V> {
63 capacity: NonZeroUsize,
64 map: HashMap<K, Entry<V>>,
65}
66
67impl<K, V> Inner<K, V>
68where
69 K: Eq + Hash + Clone,
70{
71 fn with_capacity(capacity: NonZeroUsize) -> Self {
72 let cap = capacity.get();
73 Self {
74 capacity,
75 map: HashMap::with_capacity(cap),
76 }
77 }
78}
79
80impl<K, V> TtlCache<K, V>
81where
82 K: Eq + Hash + Clone,
83 V: Clone,
84{
85 pub fn new(capacity: usize, ttl: Duration) -> Result<Self, CacheError> {
100 let cap = NonZeroUsize::new(capacity).ok_or(CacheError::InvalidCapacity)?;
101 Ok(Self::with_capacity(cap, ttl))
102 }
103
104 pub fn with_capacity(capacity: NonZeroUsize, ttl: Duration) -> Self {
119 let num_shards = sharding::shard_count(capacity);
120 let per_shard = sharding::per_shard_capacity(capacity, num_shards);
121 let sharded = Sharded::from_factory(num_shards, |_| Inner::with_capacity(per_shard));
122 Self {
123 capacity,
124 default_ttl: ttl,
125 sharded,
126 }
127 }
128
129 pub fn insert_with_ttl(&self, key: K, value: V, ttl: Duration) -> Option<V> {
149 let deadline = compute_deadline(ttl);
150 let mut inner = self.sharded.shard_for(&key).lock_recover();
151 Self::insert_inner(&mut inner, key, value, deadline)
152 }
153
154 fn insert_inner(inner: &mut Inner<K, V>, key: K, value: V, deadline: Instant) -> Option<V> {
155 let now = Instant::now();
156
157 if let Some(existing) = inner.map.get_mut(&key) {
158 if existing.expires_at > now {
159 let old = core::mem::replace(&mut existing.value, value);
160 existing.expires_at = deadline;
161 return Some(old);
162 }
163 }
164
165 let _ = inner.map.remove(&key);
166
167 if inner.map.len() >= inner.capacity.get() {
168 if let Some(victim) = find_victim(&inner.map) {
169 let _ = inner.map.remove(&victim);
170 }
171 }
172
173 let _ = inner.map.insert(
174 key,
175 Entry {
176 value,
177 expires_at: deadline,
178 },
179 );
180 None
181 }
182}
183
184impl<K, V> Cache<K, V> for TtlCache<K, V>
185where
186 K: Eq + Hash + Clone,
187 V: Clone,
188{
189 fn get(&self, key: &K) -> Option<V> {
190 let mut inner = self.sharded.shard_for(key).lock_recover();
191 let now = Instant::now();
192 let expires_at = inner.map.get(key).map(|e| e.expires_at)?;
193 if expires_at <= now {
194 let _ = inner.map.remove(key);
195 return None;
196 }
197 inner.map.get(key).map(|e| e.value.clone())
198 }
199
200 fn insert(&self, key: K, value: V) -> Option<V> {
201 let deadline = compute_deadline(self.default_ttl);
202 let mut inner = self.sharded.shard_for(&key).lock_recover();
203 Self::insert_inner(&mut inner, key, value, deadline)
204 }
205
206 fn remove(&self, key: &K) -> Option<V> {
207 let mut inner = self.sharded.shard_for(key).lock_recover();
208 inner.map.remove(key).map(|e| e.value)
209 }
210
211 fn contains_key(&self, key: &K) -> bool {
212 let mut inner = self.sharded.shard_for(key).lock_recover();
213 let now = Instant::now();
214 let Some(expires_at) = inner.map.get(key).map(|e| e.expires_at) else {
215 return false;
216 };
217 if expires_at > now {
218 return true;
219 }
220 let _ = inner.map.remove(key);
221 false
222 }
223
224 fn len(&self) -> usize {
225 let mut total = 0;
226 for mutex in self.sharded.iter() {
227 let mut inner = mutex.lock_recover();
228 purge_expired(&mut inner.map);
229 total += inner.map.len();
230 }
231 total
232 }
233
234 fn clear(&self) {
235 for mutex in self.sharded.iter() {
236 let mut inner = mutex.lock_recover();
237 inner.map.clear();
238 }
239 }
240
241 fn capacity(&self) -> usize {
242 self.capacity.get()
243 }
244}
245
246fn compute_deadline(ttl: Duration) -> Instant {
247 let now = Instant::now();
248 match now.checked_add(ttl) {
249 Some(t) => t,
250 None => now.checked_add(FAR_FUTURE).unwrap_or(now),
251 }
252}
253
254fn find_victim<K, V>(map: &HashMap<K, Entry<V>>) -> Option<K>
255where
256 K: Clone,
257{
258 map.iter()
259 .min_by_key(|(_, e)| e.expires_at)
260 .map(|(k, _)| k.clone())
261}
262
263fn purge_expired<K, V>(map: &mut HashMap<K, Entry<V>>) {
264 let now = Instant::now();
265 map.retain(|_, entry| entry.expires_at > now);
266}