1use super::{CacheBackend, Result, reservation::ReservationSet};
8use async_trait::async_trait;
9use moka::future::Cache;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13const MEMORY_CACHE_MAX_BYTES: u64 = 64 * 1024 * 1024;
14
15pub struct MemoryCache {
17 cache: Cache<String, MemoryCacheValue>,
18 default_ttl: u64,
19 reservations: ReservationSet,
20}
21
22#[derive(Clone)]
23struct MemoryCacheValue {
24 bytes: Vec<u8>,
25 expires_at: Instant,
26}
27
28impl MemoryCacheValue {
29 fn new(bytes: Vec<u8>, ttl_secs: u64) -> Self {
30 let now = Instant::now();
31 Self {
32 bytes,
33 expires_at: now
34 .checked_add(Duration::from_secs(ttl_secs))
35 .unwrap_or(now),
36 }
37 }
38
39 fn is_expired(&self) -> bool {
40 self.expires_at <= Instant::now()
41 }
42}
43
44struct MemoryCacheExpiry;
51
52impl moka::Expiry<String, MemoryCacheValue> for MemoryCacheExpiry {
53 fn expire_after_create(
54 &self,
55 _key: &String,
56 value: &MemoryCacheValue,
57 created_at: Instant,
58 ) -> Option<Duration> {
59 Some(value.expires_at.saturating_duration_since(created_at))
60 }
61
62 fn expire_after_update(
63 &self,
64 _key: &String,
65 value: &MemoryCacheValue,
66 updated_at: Instant,
67 _duration_until_expiry: Option<Duration>,
68 ) -> Option<Duration> {
69 Some(value.expires_at.saturating_duration_since(updated_at))
70 }
71}
72
73impl MemoryCache {
74 #[must_use]
76 pub fn new(default_ttl: u64) -> Self {
77 let cache = Cache::builder()
78 .max_capacity(MEMORY_CACHE_MAX_BYTES)
79 .weigher(|key: &String, value: &MemoryCacheValue| {
80 entry_weight(key.len(), value.bytes.len())
81 })
82 .expire_after(MemoryCacheExpiry)
83 .build();
84 Self {
85 cache,
86 default_ttl,
87 reservations: ReservationSet::new(default_ttl),
88 }
89 }
90
91 fn cache_value(&self, value: Vec<u8>, ttl_secs: Option<u64>) -> MemoryCacheValue {
92 MemoryCacheValue::new(value, ttl_secs.unwrap_or(self.default_ttl))
93 }
94}
95
96fn entry_weight(key_len: usize, value_len: usize) -> u32 {
97 let total = key_len.saturating_add(value_len);
98 u32::try_from(total).unwrap_or(u32::MAX)
99}
100
101#[async_trait]
102impl CacheBackend for MemoryCache {
103 fn backend_name(&self) -> &'static str {
104 "memory"
105 }
106
107 async fn health_check(&self) -> Result<()> {
108 Ok(())
109 }
110
111 async fn get_bytes(&self, key: &str) -> Option<Vec<u8>> {
112 let value = self.cache.get(key).await?;
113 if value.is_expired() {
114 self.reservations.remove(key);
120 return None;
121 }
122 Some(value.bytes)
123 }
124
125 async fn take_bytes(&self, key: &str) -> Option<Vec<u8>> {
126 self.reservations.remove(key);
127 let value = self.cache.remove(key).await?;
128 if value.is_expired() {
129 return None;
130 }
131 Some(value.bytes)
132 }
133
134 async fn set_bytes(&self, key: &str, value: Vec<u8>, ttl_secs: Option<u64>) {
135 self.cache
136 .insert(key.to_string(), self.cache_value(value, ttl_secs))
137 .await;
138 }
139
140 async fn set_bytes_if_absent(&self, key: &str, value: Vec<u8>, ttl_secs: Option<u64>) -> bool {
141 if self.get_bytes(key).await.is_some() {
142 return false;
143 }
144 let Some(guard) = self.reservations.reserve_guarded(key, ttl_secs) else {
145 return false;
146 };
147 if self.get_bytes(key).await.is_some() {
148 return false;
153 }
154
155 self.cache
156 .insert(key.to_string(), self.cache_value(value, ttl_secs))
157 .await;
158 guard.commit();
161 true
162 }
163
164 async fn delete(&self, key: &str) {
165 self.reservations.remove(key);
166 self.cache.remove(key).await;
167 }
168
169 async fn delete_many(&self, keys: &[String]) {
170 for key in keys {
171 self.delete(key).await;
172 }
173 }
174
175 async fn invalidate_prefix(&self, prefix: &str) {
176 self.reservations.invalidate_prefix(prefix);
177 let keys: Vec<Arc<String>> = self
178 .cache
179 .iter()
180 .filter(|(k, _)| k.starts_with(prefix))
181 .map(|(k, _)| k.clone())
182 .collect();
183 for key in keys {
184 self.cache.remove(key.as_ref()).await;
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::{CacheBackend, MemoryCache, entry_weight};
192 use std::sync::Arc;
193 use std::time::Duration;
194
195 #[test]
196 fn entry_weight_counts_key_and_value_bytes() {
197 assert_eq!(entry_weight(3, 5), 8);
198 }
199
200 #[test]
201 fn entry_weight_saturates_at_u32_max() {
202 assert_eq!(entry_weight(usize::MAX, usize::MAX), u32::MAX);
203 }
204
205 #[tokio::test]
206 async fn set_bytes_if_absent_allows_one_concurrent_insert() {
207 let cache = Arc::new(MemoryCache::new(60));
208 let mut tasks = Vec::new();
209 for _ in 0..16 {
210 let cache = cache.clone();
211 tasks.push(tokio::spawn(async move {
212 cache
213 .set_bytes_if_absent("nonce", Vec::new(), Some(60))
214 .await
215 }));
216 }
217
218 let successes = futures::future::join_all(tasks)
219 .await
220 .into_iter()
221 .map(|result| result.expect("reservation task should not panic"))
222 .filter(|inserted| *inserted)
223 .count();
224
225 assert_eq!(successes, 1);
226 }
227
228 #[tokio::test]
229 async fn set_bytes_if_absent_keeps_reservation_after_successful_insert() {
230 let cache = MemoryCache::new(60);
231
232 assert!(
233 cache
234 .set_bytes_if_absent("nonce", b"claimed".to_vec(), Some(60))
235 .await
236 );
237
238 assert!(!cache.reservations.reserve("nonce", Some(60)));
240 }
241
242 #[tokio::test]
243 async fn set_bytes_if_absent_takes_no_reservation_when_value_is_already_visible() {
244 let cache = MemoryCache::new(60);
245 cache.set_bytes("nonce", b"plain".to_vec(), Some(60)).await;
246
247 assert!(
248 !cache
249 .set_bytes_if_absent("nonce", b"claimed".to_vec(), Some(60))
250 .await
251 );
252
253 assert!(cache.reservations.reserve("nonce", Some(60)));
257 }
258
259 #[tokio::test]
260 async fn set_bytes_if_absent_respects_existing_set_value() {
261 let cache = MemoryCache::new(60);
262
263 cache.set_bytes("nonce", b"first".to_vec(), Some(60)).await;
264
265 assert!(
266 !cache
267 .set_bytes_if_absent("nonce", b"second".to_vec(), Some(60))
268 .await
269 );
270 assert_eq!(cache.get_bytes("nonce").await, Some(b"first".to_vec()));
271 }
272
273 #[tokio::test]
274 async fn set_bytes_respects_entry_ttl() {
275 let cache = MemoryCache::new(60);
276
277 cache.set_bytes("short", b"value".to_vec(), Some(0)).await;
278
279 assert_eq!(cache.get_bytes("short").await, None);
280 }
281
282 #[test]
283 fn expiry_derives_duration_from_absolute_expires_at() {
284 use moka::Expiry;
285
286 let expiry = super::MemoryCacheExpiry;
287 let value = super::MemoryCacheValue::new(b"value".to_vec(), 60);
288 let zero_ttl = super::MemoryCacheValue::new(b"value".to_vec(), 0);
289 let created_at = std::time::Instant::now();
291
292 let ttl = expiry
293 .expire_after_create(&"key".to_string(), &value, created_at)
294 .expect("entry should carry an expiration");
295 assert!(ttl > Duration::from_secs(59) && ttl <= Duration::from_mins(1));
296
297 assert_eq!(
299 expiry.expire_after_create(&"key".to_string(), &zero_ttl, created_at),
300 Some(Duration::ZERO)
301 );
302 }
303
304 #[tokio::test]
305 async fn per_entry_ttl_outlives_shorter_default_ttl() {
306 let cache = MemoryCache::new(1);
307 cache.set_bytes("default", b"default".to_vec(), None).await;
308 cache.set_bytes("long", b"long".to_vec(), Some(2)).await;
309
310 tokio::time::sleep(Duration::from_millis(1_100)).await;
312
313 assert_eq!(cache.get_bytes("default").await, None);
316 assert_eq!(cache.get_bytes("long").await, Some(b"long".to_vec()));
317
318 tokio::time::sleep(Duration::from_secs(1)).await;
319 assert_eq!(cache.get_bytes("long").await, None);
320 }
321
322 #[tokio::test]
323 async fn zero_default_ttl_still_stores_explicit_entry_ttl() {
324 let cache = MemoryCache::new(0);
325
326 cache
327 .set_bytes("explicit", b"value".to_vec(), Some(60))
328 .await;
329 cache.set_bytes("implicit", b"value".to_vec(), None).await;
330
331 assert_eq!(cache.get_bytes("explicit").await, Some(b"value".to_vec()));
333 assert_eq!(cache.get_bytes("implicit").await, None);
334 }
335
336 #[tokio::test]
337 async fn set_bytes_if_absent_can_replace_expired_entry() {
338 let cache = MemoryCache::new(60);
339
340 cache.set_bytes("nonce", b"expired".to_vec(), Some(0)).await;
341
342 assert!(
343 cache
344 .set_bytes_if_absent("nonce", b"fresh".to_vec(), Some(60))
345 .await
346 );
347 assert_eq!(cache.get_bytes("nonce").await, Some(b"fresh".to_vec()));
348 }
349
350 #[tokio::test]
351 async fn take_bytes_consumes_existing_entry_once() {
352 let cache = MemoryCache::new(60);
353
354 cache
355 .set_bytes("challenge", b"value".to_vec(), Some(60))
356 .await;
357
358 assert_eq!(cache.take_bytes("challenge").await, Some(b"value".to_vec()));
359 assert_eq!(cache.take_bytes("challenge").await, None);
360 assert_eq!(cache.get_bytes("challenge").await, None);
361 }
362
363 #[tokio::test]
364 async fn take_bytes_returns_none_for_missing_or_expired_entry() {
365 let cache = MemoryCache::new(60);
366
367 assert_eq!(cache.take_bytes("missing").await, None);
368 cache.set_bytes("expired", b"value".to_vec(), Some(0)).await;
369
370 assert_eq!(cache.take_bytes("expired").await, None);
371 assert_eq!(cache.get_bytes("expired").await, None);
372 }
373
374 #[tokio::test]
375 async fn take_bytes_allows_one_concurrent_consumer() {
376 let cache = Arc::new(MemoryCache::new(60));
377 cache
378 .set_bytes("challenge", b"value".to_vec(), Some(60))
379 .await;
380 let mut tasks = Vec::new();
381 for _ in 0..16 {
382 let cache = cache.clone();
383 tasks.push(tokio::spawn(
384 async move { cache.take_bytes("challenge").await },
385 ));
386 }
387
388 let values = futures::future::join_all(tasks)
389 .await
390 .into_iter()
391 .map(|result| result.expect("take task should not panic"))
392 .collect::<Vec<_>>();
393
394 assert_eq!(
395 values
396 .iter()
397 .filter(|value| value.as_deref() == Some(b"value".as_slice()))
398 .count(),
399 1
400 );
401 assert_eq!(values.iter().filter(|value| value.is_none()).count(), 15);
402 }
403
404 #[tokio::test]
405 async fn delete_many_removes_only_requested_entries() {
406 let cache = MemoryCache::new(60);
407 cache.set_bytes("remove:1", b"one".to_vec(), Some(60)).await;
408 cache.set_bytes("remove:2", b"two".to_vec(), Some(60)).await;
409 cache.set_bytes("keep", b"keep".to_vec(), Some(60)).await;
410
411 cache
412 .delete_many(&[
413 "remove:1".to_string(),
414 "remove:2".to_string(),
415 "remove:2".to_string(),
416 "missing".to_string(),
417 ])
418 .await;
419 cache.delete_many(&[]).await;
420
421 assert_eq!(cache.get_bytes("remove:1").await, None);
422 assert_eq!(cache.get_bytes("remove:2").await, None);
423 assert_eq!(cache.get_bytes("keep").await, Some(b"keep".to_vec()));
424 }
425}