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