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::error::CloneableError;
13
14use super::backend::{CacheBackend, CacheEntry};
15use super::{CacheCodec, InternalCacheKey};
16
17#[derive(Clone, Debug)]
19struct MokaCacheEntry {
20 entry: CacheEntry,
21 size_bytes: usize,
22}
23
24pub(super) fn key_footprint(_key: &InternalCacheKey) -> usize {
26 std::mem::size_of::<InternalCacheKey>()
27}
28
29fn physical_size(key: &InternalCacheKey, size_bytes: usize) -> usize {
30 key_footprint(key).saturating_add(size_bytes)
31}
32
33fn weight_unit(capacity: usize) -> usize {
38 capacity.div_ceil(u32::MAX as usize).max(1)
39}
40
41fn entry_weight(key: &InternalCacheKey, size_bytes: usize, weight_unit: usize) -> u32 {
42 physical_size(key, size_bytes)
43 .div_ceil(weight_unit)
44 .try_into()
45 .unwrap_or(u32::MAX)
46}
47
48pub struct MokaCacheBackend {
53 cache: moka::future::Cache<InternalCacheKey, MokaCacheEntry>,
54 weight_unit: usize,
55}
56
57impl std::fmt::Debug for MokaCacheBackend {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.debug_struct("MokaCacheBackend")
60 .field("entry_count", &self.cache.entry_count())
61 .finish()
62 }
63}
64
65impl MokaCacheBackend {
66 pub fn with_capacity(capacity: usize) -> Self {
67 let weight_unit = weight_unit(capacity);
68 let capacity_weight = capacity.div_ceil(weight_unit) as u64;
69 let cache = moka::future::Cache::builder()
70 .max_capacity(capacity_weight)
71 .weigher(move |key: &InternalCacheKey, entry: &MokaCacheEntry| {
72 entry_weight(key, entry.size_bytes, weight_unit)
73 })
74 .build();
75 Self { cache, weight_unit }
76 }
77
78 pub fn no_cache() -> Self {
79 Self {
80 cache: moka::future::Cache::new(0),
81 weight_unit: 1,
82 }
83 }
84
85 fn weighted_size_bytes(&self) -> usize {
86 self.cache
87 .weighted_size()
88 .saturating_mul(self.weight_unit as u64)
89 .try_into()
90 .unwrap_or(usize::MAX)
91 }
92}
93
94#[async_trait]
95impl CacheBackend for MokaCacheBackend {
96 async fn get(&self, key: &InternalCacheKey, _codec: Option<CacheCodec>) -> Option<CacheEntry> {
97 self.cache.get(key).await.map(|r| r.entry)
98 }
99
100 async fn insert(
101 &self,
102 key: &InternalCacheKey,
103 entry: CacheEntry,
104 size_bytes: usize,
105 _codec: Option<CacheCodec>,
106 ) {
107 self.cache
108 .insert(*key, MokaCacheEntry { entry, size_bytes })
109 .await;
110 }
111
112 async fn get_or_insert<'a>(
113 &self,
114 key: &InternalCacheKey,
115 loader: Pin<Box<dyn Future<Output = Result<(CacheEntry, usize)>> + Send + 'a>>,
116 _codec: Option<CacheCodec>,
117 ) -> Result<(CacheEntry, bool)> {
118 let was_miss = Arc::new(AtomicBool::new(false));
120 let was_miss_clone = was_miss.clone();
121
122 let init = async move {
123 was_miss_clone.store(true, Ordering::Relaxed);
124 loader
125 .await
126 .map(|(entry, size_bytes)| MokaCacheEntry { entry, size_bytes })
127 .map_err(CloneableError)
128 };
129
130 let owned_key = *key;
131 match self.cache.try_get_with(owned_key, init).await {
132 Ok(record) => {
133 let was_cached = !was_miss.load(Ordering::Relaxed);
134 Ok((record.entry, was_cached))
135 }
136 Err(error) => Err(Arc::unwrap_or_clone(error).0),
137 }
138 }
139
140 async fn clear(&self) {
141 self.cache.invalidate_all();
142 self.cache.run_pending_tasks().await;
143 }
144
145 async fn num_entries(&self) -> usize {
146 self.cache.run_pending_tasks().await;
147 self.cache.entry_count() as usize
148 }
149
150 async fn size_bytes(&self) -> usize {
151 self.cache.run_pending_tasks().await;
152 self.weighted_size_bytes()
153 }
154
155 fn approx_num_entries(&self) -> usize {
156 self.cache.entry_count() as usize
157 }
158
159 fn approx_size_bytes(&self) -> usize {
160 self.weighted_size_bytes()
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn entry_weights_are_exact_at_byte_granularity() {
172 let key = InternalCacheKey::from_bytes([0; 16]);
173 assert_eq!(weight_unit(4096), 1);
174 assert_eq!(entry_weight(&key, 7, 1), 23);
175 }
176
177 #[tokio::test]
178 async fn size_methods_use_constant_time_weighted_accounting() {
179 let backend = MokaCacheBackend::with_capacity(4096);
180 let key = InternalCacheKey::from_bytes([0; 16]);
181 let entry: CacheEntry = Arc::new(());
182 let value_size = 7;
183 let expected = physical_size(&key, value_size);
184
185 backend.insert(&key, entry, value_size, None).await;
186
187 assert_eq!(backend.size_bytes().await, expected);
188 assert_eq!(backend.approx_size_bytes(), expected);
189 }
190
191 #[cfg(target_pointer_width = "64")]
192 #[test]
193 fn entry_weights_scale_for_capacities_above_four_gibibytes() {
194 let key = InternalCacheKey::from_bytes([0; 16]);
195 let capacity = 6 * 1024 * 1024 * 1024;
196 let weight_unit = weight_unit(capacity);
197 assert_eq!(weight_unit, 2);
198
199 let size_bytes = u32::MAX as usize + 1024;
200 let expected = physical_size(&key, size_bytes).div_ceil(weight_unit);
201 let weight = entry_weight(&key, size_bytes, weight_unit);
202 assert_eq!(weight as usize, expected);
203 assert_ne!(weight, u32::MAX);
204 }
205}