1use std::pin::Pin;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8use async_trait::async_trait;
9use futures::Future;
10
11use crate::Result;
12use crate::deepsize::Context;
13use crate::error::CloneableError;
14
15use super::backend::{CacheBackend, CacheEntry};
16use super::{CacheCodec, InternalCacheKey};
17
18#[derive(Clone, Debug)]
20struct MokaCacheEntry {
21 entry: CacheEntry,
22 size_bytes: usize,
23}
24
25pub(super) fn key_footprint(_key: &InternalCacheKey) -> usize {
27 std::mem::size_of::<InternalCacheKey>()
28}
29
30fn physical_size(key: &InternalCacheKey, size_bytes: usize) -> usize {
31 key_footprint(key).saturating_add(size_bytes)
32}
33
34fn weight_unit(capacity: usize) -> usize {
39 capacity.div_ceil(u32::MAX as usize).max(1)
40}
41
42fn entry_weight(key: &InternalCacheKey, size_bytes: usize, weight_unit: usize) -> u32 {
43 physical_size(key, size_bytes)
44 .div_ceil(weight_unit)
45 .try_into()
46 .unwrap_or(u32::MAX)
47}
48
49pub struct MokaCacheBackend {
54 cache: moka::future::Cache<InternalCacheKey, MokaCacheEntry>,
55 capacity: usize,
56 weight_unit: usize,
57}
58
59impl std::fmt::Debug for MokaCacheBackend {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("MokaCacheBackend")
62 .field("entry_count", &self.cache.entry_count())
63 .finish()
64 }
65}
66
67impl MokaCacheBackend {
68 pub fn with_capacity(capacity: usize) -> Self {
69 let weight_unit = weight_unit(capacity);
70 let capacity_weight = capacity.div_ceil(weight_unit) as u64;
71 let cache = moka::future::Cache::builder()
72 .max_capacity(capacity_weight)
73 .weigher(move |key: &InternalCacheKey, entry: &MokaCacheEntry| {
74 entry_weight(key, entry.size_bytes, weight_unit)
75 })
76 .build();
77 Self {
78 cache,
79 capacity,
80 weight_unit,
81 }
82 }
83
84 pub fn no_cache() -> Self {
85 Self {
86 cache: moka::future::Cache::new(0),
87 capacity: 0,
88 weight_unit: 1,
89 }
90 }
91
92 pub fn capacity(&self) -> usize {
94 self.capacity
95 }
96
97 fn weighted_size_bytes(&self) -> usize {
98 self.cache
99 .weighted_size()
100 .saturating_mul(self.weight_unit as u64)
101 .try_into()
102 .unwrap_or(usize::MAX)
103 }
104}
105
106#[async_trait]
107impl CacheBackend for MokaCacheBackend {
108 async fn get_resident(&self, key: &InternalCacheKey) -> Option<CacheEntry> {
109 self.cache.get(key).await.map(|r| r.entry)
110 }
111
112 async fn peek_resident(&self, key: &InternalCacheKey) -> bool {
115 self.cache.contains_key(key)
116 }
117
118 async fn get(&self, key: &InternalCacheKey, _codec: Option<CacheCodec>) -> Option<CacheEntry> {
119 self.cache.get(key).await.map(|r| r.entry)
120 }
121
122 async fn insert(
123 &self,
124 key: &InternalCacheKey,
125 entry: CacheEntry,
126 size_bytes: usize,
127 _codec: Option<CacheCodec>,
128 ) {
129 self.cache
130 .insert(*key, MokaCacheEntry { entry, size_bytes })
131 .await;
132 }
133
134 async fn get_or_insert<'a>(
135 &self,
136 key: &InternalCacheKey,
137 loader: Pin<Box<dyn Future<Output = Result<(CacheEntry, usize)>> + Send + 'a>>,
138 _codec: Option<CacheCodec>,
139 ) -> Result<(CacheEntry, bool)> {
140 if self.capacity == 0 {
142 return loader.await.map(|(entry, _)| (entry, false));
143 }
144
145 let was_miss = Arc::new(AtomicBool::new(false));
147 let was_miss_clone = was_miss.clone();
148
149 let init = async move {
150 was_miss_clone.store(true, Ordering::Relaxed);
151 loader
152 .await
153 .map(|(entry, size_bytes)| MokaCacheEntry { entry, size_bytes })
154 .map_err(CloneableError::new)
155 };
156
157 let owned_key = *key;
158 match self.cache.try_get_with(owned_key, init).await {
159 Ok(record) => {
160 let was_cached = !was_miss.load(Ordering::Relaxed);
161 Ok((record.entry, was_cached))
162 }
163 Err(error) => Err(Arc::unwrap_or_clone(error).into_inner()),
164 }
165 }
166
167 async fn clear(&self) {
168 self.cache.invalidate_all();
169 self.cache.run_pending_tasks().await;
170 }
171
172 async fn num_entries(&self) -> usize {
173 self.cache.run_pending_tasks().await;
174 self.cache.entry_count() as usize
175 }
176
177 async fn size_bytes(&self) -> usize {
178 self.cache.run_pending_tasks().await;
179 self.weighted_size_bytes()
180 }
181
182 fn capacity_bytes(&self) -> Option<usize> {
183 Some(self.capacity)
184 }
185
186 fn approx_num_entries(&self) -> usize {
187 self.cache.entry_count() as usize
188 }
189
190 fn approx_size_bytes(&self) -> usize {
191 self.weighted_size_bytes()
194 }
195
196 fn deep_size_of_entries(
197 &self,
198 context: &mut Context,
199 size_of_entry: &dyn Fn(&CacheEntry, &mut Context) -> Option<usize>,
200 ) -> Option<usize> {
201 Some(
202 self.cache
203 .iter()
204 .map(|(key, record)| {
205 key_footprint(key.as_ref())
206 + size_of_entry(&record.entry, context).unwrap_or(record.size_bytes)
207 })
208 .sum(),
209 )
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use std::pin::pin;
216
217 use futures::{FutureExt, poll};
218 use tokio::sync::oneshot;
219
220 use super::*;
221
222 #[rstest::rstest]
223 #[case::zero_capacity_success(MokaCacheBackend::with_capacity(0), false)]
224 #[case::zero_capacity_error(MokaCacheBackend::with_capacity(0), true)]
225 #[case::no_cache_success(MokaCacheBackend::no_cache(), false)]
226 #[case::no_cache_error(MokaCacheBackend::no_cache(), true)]
227 #[tokio::test]
228 async fn zero_capacity_loaders_run_independently(
229 #[case] backend: MokaCacheBackend,
230 #[case] loader_fails: bool,
231 ) {
232 let key = InternalCacheKey::from_bytes([0; 16]);
233 let first_entry: CacheEntry = Arc::new(1_u64);
234 let second_entry: CacheEntry = Arc::new(2_u64);
235 let (release_tx, release_rx) = oneshot::channel();
236 let entry = first_entry.clone();
237 let mut first = pin!(backend.get_or_insert(
238 &key,
239 Box::pin(async move {
240 release_rx.await.unwrap();
241 Ok((entry, 8))
242 }),
243 None,
244 ));
245 assert!(poll!(first.as_mut()).is_pending());
246
247 let entry = second_entry.clone();
248 let second = backend
249 .get_or_insert(
250 &key,
251 Box::pin(async move {
252 if loader_fails {
253 Err(crate::Error::invalid_input("independent loader failed"))
254 } else {
255 Ok((entry, 8))
256 }
257 }),
258 None,
259 )
260 .now_or_never()
261 .expect("a disabled cache must not wait for another loader on the same key");
262 if loader_fails {
263 let error = second.unwrap_err();
264 assert!(matches!(&error, crate::Error::InvalidInput { .. }));
265 assert!(error.to_string().contains("independent loader failed"));
266 } else {
267 let (entry, was_cached) = second.unwrap();
268 assert!(Arc::ptr_eq(&entry, &second_entry));
269 assert!(!was_cached);
270 }
271
272 release_tx.send(()).unwrap();
273 let (entry, was_cached) = first.await.unwrap();
274 assert!(Arc::ptr_eq(&entry, &first_entry));
275 assert!(!was_cached);
276 assert!(backend.get(&key, None).await.is_none());
277 assert_eq!(backend.num_entries().await, 0);
278 assert_eq!(backend.size_bytes().await, 0);
279 }
280
281 #[test]
282 fn entry_weights_are_exact_at_byte_granularity() {
283 let key = InternalCacheKey::from_bytes([0; 16]);
284 assert_eq!(weight_unit(4096), 1);
285 assert_eq!(entry_weight(&key, 7, 1), 23);
286 }
287
288 #[test]
289 fn capacity_bytes_reports_configured_capacity() {
290 assert_eq!(
291 MokaCacheBackend::with_capacity(4096).capacity_bytes(),
292 Some(4096)
293 );
294 assert_eq!(MokaCacheBackend::no_cache().capacity_bytes(), Some(0));
295 }
296
297 #[tokio::test]
298 async fn size_methods_use_constant_time_weighted_accounting() {
299 let backend = MokaCacheBackend::with_capacity(4096);
300 let key = InternalCacheKey::from_bytes([0; 16]);
301 let entry: CacheEntry = Arc::new(());
302 let value_size = 7;
303 let expected = physical_size(&key, value_size);
304
305 backend.insert(&key, entry, value_size, None).await;
306
307 assert_eq!(backend.size_bytes().await, expected);
308 assert_eq!(backend.approx_size_bytes(), expected);
309 }
310
311 #[cfg(target_pointer_width = "64")]
312 #[test]
313 fn entry_weights_scale_for_capacities_above_four_gibibytes() {
314 let key = InternalCacheKey::from_bytes([0; 16]);
315 let capacity = 6 * 1024 * 1024 * 1024;
316 let weight_unit = weight_unit(capacity);
317 assert_eq!(weight_unit, 2);
318
319 let size_bytes = u32::MAX as usize + 1024;
320 let expected = physical_size(&key, size_bytes).div_ceil(weight_unit);
321 let weight = entry_weight(&key, size_bytes, weight_unit);
322 assert_eq!(weight as usize, expected);
323 assert_ne!(weight, u32::MAX);
324 }
325}
326
327pub const MOKA_BACKEND_KIND: &str = "moka";
329
330pub(super) fn build_moka_backend(
339 config: &super::registry::BackendConfig,
340) -> Result<MokaCacheBackend> {
341 let mut capacity: Option<usize> = None;
342 for (key, value) in &config.options {
343 match key.as_str() {
344 "capacity" => {
345 if value.is_empty() {
346 return Err(crate::Error::invalid_input(
347 "moka cache backend: capacity must not be empty",
348 ));
349 } else {
350 capacity = Some(value.parse::<usize>().map_err(|err| {
351 crate::Error::invalid_input(format!(
352 "moka cache backend: cannot parse capacity {:?}: {}",
353 value, err
354 ))
355 })?);
356 }
357 }
358 other => {
359 return Err(crate::Error::invalid_input(format!(
360 "moka cache backend: unknown option {:?}",
361 other
362 )));
363 }
364 }
365 }
366 let capacity = capacity.ok_or_else(|| {
367 crate::Error::invalid_input(
368 "moka cache backend: capacity is required; use moka://?capacity=<bytes>",
369 )
370 })?;
371 Ok(MokaCacheBackend::with_capacity(capacity))
372}
373
374pub(super) fn build_moka(config: &super::registry::BackendConfig) -> Result<Arc<dyn CacheBackend>> {
375 Ok(Arc::new(build_moka_backend(config)?))
376}
377
378#[cfg(test)]
379mod moka_registry_tests {
380 use super::super::backend_uri::{build_from_uri, parse_backend_uri};
381 use super::super::registry::{BackendConfig, build_from_config, registry_test_lock};
382 use super::*;
383
384 #[test]
385 fn test_moka_builds_from_config() {
386 let _lock = registry_test_lock();
387 let cfg = BackendConfig::new("moka")
388 .unwrap()
389 .with_option("capacity", "1048576");
390 let backend = build_moka_backend(&cfg).unwrap();
391 assert_eq!(backend.capacity(), 1048576);
392 let _backend = build_from_config(&cfg).unwrap();
393 }
394
395 #[test]
396 fn test_moka_builds_from_uri() {
397 let _lock = registry_test_lock();
398 let cfg = parse_backend_uri("moka://?capacity=1048576").unwrap();
399 let backend = build_moka_backend(&cfg).unwrap();
400 assert_eq!(backend.capacity(), 1048576);
401 let _backend = build_from_uri("moka://?capacity=1048576").unwrap();
402 }
403
404 #[test]
405 fn test_moka_rejects_unknown_option() {
406 let _lock = registry_test_lock();
407 let cfg = BackendConfig::new("moka")
408 .unwrap()
409 .with_option("mystery", "1");
410 let err = build_from_config(&cfg).unwrap_err();
411 assert!(err.to_string().contains("unknown option"));
412 }
413
414 #[test]
415 fn test_moka_rejects_bad_capacity() {
416 let _lock = registry_test_lock();
417 let cfg = BackendConfig::new("moka")
418 .unwrap()
419 .with_option("capacity", "not-a-number");
420 let err = build_from_config(&cfg).unwrap_err();
421 assert!(err.to_string().contains("cannot parse capacity"));
422 }
423
424 #[test]
425 fn test_moka_rejects_missing_capacity() {
426 let _lock = registry_test_lock();
427 let cfg = BackendConfig::new("moka").unwrap();
428 let err = build_from_config(&cfg).unwrap_err();
429 assert!(err.to_string().contains("capacity is required"));
430 }
431
432 #[test]
433 fn test_moka_rejects_empty_capacity() {
434 let _lock = registry_test_lock();
435 let cfg = BackendConfig::new("moka")
436 .unwrap()
437 .with_option("capacity", "");
438 let err = build_from_config(&cfg).unwrap_err();
439 assert!(err.to_string().contains("capacity must not be empty"));
440 }
441}