crates_docs/cache/
memory.rs1use std::sync::Arc;
7use std::time::Duration;
8
9#[derive(Clone, Debug)]
11struct CacheEntry {
12 value: Arc<str>,
13 ttl: Option<Duration>,
14}
15
16#[derive(Debug, Clone, Default)]
18struct CacheExpiry;
19
20impl moka::Expiry<String, CacheEntry> for CacheExpiry {
21 fn expire_after_create(
22 &self,
23 _key: &String,
24 value: &CacheEntry,
25 _created_at: std::time::Instant,
26 ) -> Option<Duration> {
27 value.ttl
28 }
29}
30
31pub struct MemoryCache {
39 cache: moka::sync::Cache<String, CacheEntry>,
40}
41
42impl MemoryCache {
43 #[must_use]
48 pub fn new(max_size: usize) -> Self {
49 Self {
50 cache: moka::sync::Cache::builder()
51 .max_capacity(max_size as u64)
52 .expire_after(CacheExpiry)
53 .build(),
54 }
55 }
56
57 #[cfg(test)]
63 pub fn run_pending_tasks(&self) {
64 self.cache.run_pending_tasks();
65 }
66
67 #[cfg(test)]
73 #[must_use]
74 pub fn entry_count(&self) -> usize {
75 usize::try_from(self.cache.entry_count()).expect("cache entry count should fit in usize")
76 }
77}
78
79#[async_trait::async_trait]
80impl super::Cache for MemoryCache {
81 #[tracing::instrument(skip(self), level = "trace")]
82 async fn get(&self, key: &str) -> Option<Arc<str>> {
83 let result = self.cache.get(key).map(|entry| Arc::clone(&entry.value));
84 if result.is_some() {
85 tracing::trace!(cache_type = "memory", key = %key, "Cache hit");
86 } else {
87 tracing::trace!(cache_type = "memory", key = %key, "Cache miss");
88 }
89 result
90 }
91
92 #[tracing::instrument(skip(self), level = "trace")]
93 async fn set(
94 &self,
95 key: String,
96 value: String,
97 ttl: Option<Duration>,
98 ) -> crate::error::Result<()> {
99 let entry = CacheEntry {
100 value: Arc::from(value.into_boxed_str()),
101 ttl,
102 };
103 tracing::trace!(cache_type = "memory", key = %key, "Setting cache entry");
104 self.cache.insert(key, entry);
105 Ok(())
106 }
107
108 #[tracing::instrument(skip(self), level = "trace")]
109 async fn delete(&self, key: &str) -> crate::error::Result<()> {
110 tracing::trace!(cache_type = "memory", key = %key, "Deleting cache entry");
111 self.cache.invalidate(key);
112 Ok(())
113 }
114
115 #[tracing::instrument(skip(self), level = "trace")]
116 async fn clear(&self) -> crate::error::Result<()> {
117 tracing::trace!(cache_type = "memory", "Clearing all cache entries");
118 self.cache.invalidate_all();
119 Ok(())
120 }
121
122 #[tracing::instrument(skip(self), level = "trace")]
123 async fn exists(&self, key: &str) -> bool {
124 let result = self.cache.contains_key(key);
125 tracing::trace!(cache_type = "memory", key = %key, exists = result, "Checking cache entry existence");
126 result
127 }
128
129 fn as_any(&self) -> &dyn std::any::Any {
130 self
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::cache::Cache;
138 use tokio::time::sleep;
139
140 const DEFAULT_TEST_CACHE_CAPACITY: usize = 10;
142
143 const TEST_TTL_MS: u64 = 100;
145
146 const TEST_TTL_WAIT_MS: u64 = 150;
148
149 #[tokio::test]
150 async fn test_memory_cache_basic() {
151 let cache = MemoryCache::new(DEFAULT_TEST_CACHE_CAPACITY);
152
153 cache
155 .set("key1".to_string(), "value1".to_string(), None)
156 .await
157 .expect("set should succeed");
158 let result = cache.get("key1").await;
159 assert!(result.is_some());
160 assert_eq!(result.unwrap().as_ref(), "value1");
161
162 cache.delete("key1").await.expect("delete should succeed");
164 assert_eq!(cache.get("key1").await, None);
165
166 cache
168 .set("key2".to_string(), "value2".to_string(), None)
169 .await
170 .expect("set should succeed");
171 cache.clear().await.expect("clear should succeed");
172 cache.run_pending_tasks();
174 assert_eq!(cache.get("key2").await, None);
175 }
176
177 #[tokio::test]
178 async fn test_memory_cache_ttl() {
179 let cache = MemoryCache::new(DEFAULT_TEST_CACHE_CAPACITY);
180
181 cache
183 .set(
184 "key1".to_string(),
185 "value1".to_string(),
186 Some(Duration::from_millis(TEST_TTL_MS)),
187 )
188 .await
189 .expect("set should succeed");
190 let result = cache.get("key1").await;
191 assert!(result.is_some());
192 assert_eq!(result.unwrap().as_ref(), "value1");
193
194 sleep(Duration::from_millis(TEST_TTL_WAIT_MS)).await;
196 cache.run_pending_tasks();
198 assert_eq!(cache.get("key1").await, None);
199 }
200
201 #[tokio::test]
202 async fn test_memory_cache_eviction() {
203 let cache = MemoryCache::new(3);
207
208 for i in 0..5 {
210 cache
211 .set(format!("key{i}"), format!("value{i}"), None)
212 .await
213 .expect("set should succeed");
214 }
215
216 cache.run_pending_tasks();
218
219 let entry_count = cache.entry_count();
221 assert!(
222 entry_count <= 5,
223 "Entry count should be at most 5, got {entry_count}"
224 );
225 }
226
227 #[tokio::test]
228 async fn test_memory_cache_exists() {
229 let cache = MemoryCache::new(DEFAULT_TEST_CACHE_CAPACITY);
230
231 cache
232 .set("key1".to_string(), "value1".to_string(), None)
233 .await
234 .expect("set should succeed");
235 assert!(cache.exists("key1").await);
236 assert!(!cache.exists("key2").await);
237 }
238}