salvo_cache/
moka_store.rs1use std::borrow::Borrow;
3use std::convert::Infallible;
4use std::fmt::{self, Debug, Formatter};
5use std::hash::Hash;
6use std::sync::Arc;
7use std::time::Duration;
8
9use moka::future::Cache as MokaCache;
10use moka::future::CacheBuilder as MokaCacheBuilder;
11use moka::notification::RemovalCause;
12
13use super::{CacheStore, CachedEntry};
14
15pub struct Builder<K> {
17 inner: MokaCacheBuilder<K, CachedEntry, MokaCache<K, CachedEntry>>,
18}
19
20impl<K> Debug for Builder<K> {
21 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
22 f.debug_struct("Builder").finish()
23 }
24}
25impl<K> Builder<K>
26where
27 K: Hash + Eq + Send + Sync + Clone + 'static,
28{
29 #[must_use] pub fn initial_capacity(mut self, capacity: usize) -> Self {
31 self.inner = self.inner.initial_capacity(capacity);
32 self
33 }
34
35 #[must_use] pub fn max_capacity(mut self, capacity: u64) -> Self {
46 self.inner = self.inner.max_capacity(capacity);
47 self
48 }
49
50 #[must_use]
57 pub fn weigher(
58 mut self,
59 weigher: impl Fn(&K, &CachedEntry) -> u32 + Send + Sync + 'static,
60 ) -> Self {
61 self.inner = self.inner.weigher(weigher);
62 self
63 }
64
65 #[must_use] pub fn time_to_idle(mut self, duration: Duration) -> Self {
76 self.inner = self.inner.time_to_idle(duration);
77 self
78 }
79
80 #[must_use] pub fn time_to_live(mut self, duration: Duration) -> Self {
91 self.inner = self.inner.time_to_live(duration);
92 self
93 }
94
95 #[must_use]
104 pub fn eviction_listener(
105 mut self,
106 listener: impl Fn(Arc<K>, CachedEntry, RemovalCause) + Send + Sync + 'static,
107 ) -> Self {
108 self.inner = self.inner.eviction_listener(listener);
109 self
110 }
111
112 #[must_use] pub fn build(self) -> MokaStore<K> {
120 MokaStore {
121 inner: self.inner.build(),
122 }
123 }
124}
125pub struct MokaStore<K> {
127 inner: MokaCache<K, CachedEntry>,
128}
129
130impl<K> Debug for MokaStore<K> {
131 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
132 f.debug_struct("MokaStore").finish()
133 }
134}
135impl<K> MokaStore<K>
136where
137 K: Hash + Eq + Send + Sync + Clone + 'static,
138{
139 #[must_use] pub fn new(max_capacity: u64) -> Self {
141 Self {
142 inner: MokaCache::new(max_capacity),
143 }
144 }
145
146 #[must_use] pub fn builder() -> Builder<K> {
148 Builder {
149 inner: MokaCache::builder(),
150 }
151 }
152}
153
154impl<K> CacheStore for MokaStore<K>
155where
156 K: Hash + Eq + Send + Sync + Clone + 'static,
157{
158 type Error = Infallible;
159 type Key = K;
160
161 async fn load_entry<Q>(&self, key: &Q) -> Option<CachedEntry>
162 where
163 Self::Key: Borrow<Q>,
164 Q: Hash + Eq + Sync,
165 {
166 self.inner.get(key).await
167 }
168
169 async fn save_entry(&self, key: Self::Key, entry: CachedEntry) -> Result<(), Self::Error> {
170 self.inner.insert(key, entry).await;
171 Ok(())
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use std::time::Duration;
179 use salvo_core::http::{HeaderMap, StatusCode};
180 use crate::{CachedBody, CachedEntry};
181
182 #[tokio::test]
183 async fn test_moka_store() {
184 let store = MokaStore::new(100);
185 let key = "test_key".to_owned();
186 let entry = CachedEntry {
187 status: Some(StatusCode::OK),
188 headers: HeaderMap::new(),
189 body: CachedBody::Once("test_body".into()),
190 };
191 store.save_entry(key.clone(), entry.clone()).await.unwrap();
192 let loaded_entry = store.load_entry(&key).await.unwrap();
193 assert_eq!(loaded_entry.status, entry.status);
194 assert_eq!(loaded_entry.body, entry.body);
195 }
196
197 #[tokio::test]
198 async fn test_moka_store_builder() {
199 let store = MokaStore::<String>::builder()
200 .initial_capacity(50)
201 .max_capacity(100)
202 .time_to_live(Duration::from_secs(1))
203 .time_to_idle(Duration::from_secs(1))
204 .build();
205 let key = "test_key".to_owned();
206 let entry = CachedEntry {
207 status: Some(StatusCode::OK),
208 headers: HeaderMap::new(),
209 body: CachedBody::Once("test_body".into()),
210 };
211 store.save_entry(key.clone(), entry.clone()).await.unwrap();
212 let loaded_entry = store.load_entry(&key).await.unwrap();
213 assert_eq!(loaded_entry.status, entry.status);
214 assert_eq!(loaded_entry.body, entry.body);
215
216 tokio::time::sleep(Duration::from_secs(2)).await;
217 let loaded_entry = store.load_entry(&key).await;
218 assert!(loaded_entry.is_none());
219 }
220
221 #[test]
222 fn test_builder_debug() {
223 let builder = MokaStore::<String>::builder();
224 let dbg_str = format!("{builder:?}");
225 assert_eq!(dbg_str, "Builder");
226 }
227
228 #[test]
229 fn test_moka_store_debug() {
230 let store = MokaStore::<String>::new(100);
231 let dbg_str = format!("{store:?}");
232 assert_eq!(dbg_str, "MokaStore");
233 }
234
235 #[tokio::test]
236 async fn test_eviction_listener() {
237 use std::sync::atomic::{AtomicBool, Ordering};
238 let evicted = Arc::new(AtomicBool::new(false));
239 let evicted_clone = evicted.clone();
240 let store = MokaStore::<String>::builder()
241 .max_capacity(1)
242 .eviction_listener(move |_, _, _| {
243 evicted_clone.store(true, Ordering::SeqCst);
244 })
245 .build();
246 let entry = CachedEntry {
247 status: None,
248 headers: HeaderMap::new(),
249 body: CachedBody::Once("test_body".into()),
250 };
251 store.save_entry("key1".to_owned(), entry.clone()).await.unwrap();
252 store.save_entry("key2".to_owned(), entry.clone()).await.unwrap();
253
254 for _ in 0..10 {
256 store.load_entry(&"key1".to_owned()).await;
257 store.load_entry(&"key2".to_owned()).await;
258 if evicted.load(Ordering::SeqCst) {
259 break;
260 }
261 tokio::time::sleep(Duration::from_millis(50)).await;
262 }
263
264 assert!(evicted.load(Ordering::SeqCst));
265 }
266
267 #[tokio::test]
268 async fn test_weigher_bounds_by_bytes() {
269 use std::sync::atomic::{AtomicBool, Ordering};
270
271 fn body_len(entry: &CachedEntry) -> u32 {
272 match &entry.body {
273 CachedBody::None => 0,
274 CachedBody::Once(bytes) => bytes.len() as u32,
275 CachedBody::Chunks(chunks) => {
276 chunks.iter().map(|c| c.len()).sum::<usize>() as u32
277 }
278 }
279 }
280
281 let evicted = Arc::new(AtomicBool::new(false));
282 let evicted_clone = evicted.clone();
283 let store = MokaStore::<String>::builder()
285 .weigher(|_k, entry| body_len(entry))
286 .max_capacity(12)
287 .eviction_listener(move |_, _, _| {
288 evicted_clone.store(true, Ordering::SeqCst);
289 })
290 .build();
291 let entry = CachedEntry {
292 status: None,
293 headers: HeaderMap::new(),
294 body: CachedBody::Once("test_body".into()), };
296 store.save_entry("key1".to_owned(), entry.clone()).await.unwrap();
297 store.save_entry("key2".to_owned(), entry.clone()).await.unwrap();
298
299 for _ in 0..10 {
300 store.load_entry(&"key1".to_owned()).await;
301 store.load_entry(&"key2".to_owned()).await;
302 if evicted.load(Ordering::SeqCst) {
303 break;
304 }
305 tokio::time::sleep(Duration::from_millis(50)).await;
306 }
307
308 assert!(evicted.load(Ordering::SeqCst));
309 }
310}