rskit_cache/adapters/memory/
mod.rs1use std::collections::BTreeMap;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use parking_lot::Mutex;
8use rskit_errors::{AppError, AppResult, ErrorCode};
9
10use crate::config::CacheConfig;
11use crate::registry::{CacheRegistry, CacheStore, CacheStoreFactory};
12
13#[derive(Clone)]
14struct Entry {
15 value: String,
16 expires_at: Option<Instant>,
17}
18
19impl Entry {
20 fn is_expired(&self, now: Instant) -> bool {
21 self.expires_at.is_some_and(|expires_at| expires_at <= now)
22 }
23}
24
25pub struct MemoryCache {
27 prefix: Option<String>,
28 max_entries: Option<usize>,
29 entries: Mutex<BTreeMap<String, Entry>>,
30 clock: Arc<dyn Fn() -> Instant + Send + Sync>,
31}
32
33impl MemoryCache {
34 #[must_use]
36 pub fn new(prefix: Option<String>, max_entries: Option<usize>) -> Self {
37 Self::new_with_clock(prefix, max_entries, Instant::now)
38 }
39
40 #[must_use]
45 pub fn new_with_clock(
46 prefix: Option<String>,
47 max_entries: Option<usize>,
48 clock: impl Fn() -> Instant + Send + Sync + 'static,
49 ) -> Self {
50 Self {
51 prefix,
52 max_entries: max_entries.filter(|entries| *entries > 0),
53 entries: Mutex::new(BTreeMap::new()),
54 clock: Arc::new(clock),
55 }
56 }
57
58 fn now(&self) -> Instant {
59 (self.clock)()
60 }
61
62 fn key(&self, key: &str) -> String {
63 self.prefix
64 .as_ref()
65 .map_or_else(|| key.to_owned(), |prefix| format!("{prefix}:{key}"))
66 }
67
68 fn prune_expired(entries: &mut BTreeMap<String, Entry>, now: Instant) {
69 entries.retain(|_, entry| !entry.is_expired(now));
70 }
71
72 fn expires_at(now: Instant, ttl: Option<Duration>) -> AppResult<Option<Instant>> {
73 let Some(duration) = ttl else {
74 return Ok(None);
75 };
76 now.checked_add(duration).map(Some).ok_or_else(|| {
77 AppError::new(
78 ErrorCode::InvalidInput,
79 "cache TTL is too large to represent safely",
80 )
81 })
82 }
83}
84
85#[async_trait::async_trait]
86impl CacheStore for MemoryCache {
87 async fn get(&self, key: &str) -> AppResult<Option<String>> {
88 let mut entries = self.entries.lock();
89 Self::prune_expired(&mut entries, self.now());
90 let full_key = self.key(key);
91 Ok(entries.get(&full_key).map(|entry| entry.value.clone()))
92 }
93
94 async fn set(&self, key: &str, val: &str, ttl: Option<Duration>) -> AppResult<()> {
95 let mut entries = self.entries.lock();
96 let now = self.now();
97 Self::prune_expired(&mut entries, now);
98 let key = self.key(key);
99
100 if ttl.is_some_and(|ttl| ttl.is_zero()) {
101 return Err(AppError::new(
102 ErrorCode::InvalidInput,
103 "cache TTL must be greater than zero",
104 ));
105 }
106 let expires_at = Self::expires_at(now, ttl)?;
107
108 if let Some(max_entries) = self.max_entries
109 && entries.len() >= max_entries
110 && !entries.contains_key(&key)
111 && let Some(first_key) = entries.keys().next().cloned()
112 {
113 entries.remove(&first_key);
114 }
115
116 entries.insert(
117 key,
118 Entry {
119 value: val.to_owned(),
120 expires_at,
121 },
122 );
123 Ok(())
124 }
125
126 async fn delete(&self, key: &str) -> AppResult<bool> {
127 let mut entries = self.entries.lock();
128 Self::prune_expired(&mut entries, self.now());
129 let key = self.key(key);
130 Ok(entries.remove(&key).is_some())
131 }
132
133 async fn exists(&self, key: &str) -> AppResult<bool> {
134 self.get(key).await.map(|value| value.is_some())
135 }
136}
137
138impl Default for MemoryCache {
139 fn default() -> Self {
140 Self::new(None, None)
141 }
142}
143
144struct MemoryFactory;
145
146#[async_trait::async_trait]
147impl CacheStoreFactory for MemoryFactory {
148 async fn create(&self, config: &CacheConfig) -> AppResult<Arc<dyn CacheStore>> {
149 Ok(Arc::new(MemoryCache::new(
150 config.key_prefix.clone(),
151 config.memory.max_entries,
152 )))
153 }
154}
155
156pub fn register_memory(registry: &mut CacheRegistry) -> AppResult<()> {
158 registry.register("memory", Arc::new(MemoryFactory))
159}
160
161#[cfg(test)]
162mod tests {
163 use std::sync::Arc;
164
165 use parking_lot::Mutex;
166
167 use super::*;
168
169 #[tokio::test]
170 async fn set_rejects_unrepresentable_ttl() {
171 let cache = MemoryCache::default();
172
173 let err = cache
174 .set("too-long", "value", Some(Duration::MAX))
175 .await
176 .expect_err("TTL overflow must be rejected");
177
178 assert_eq!(err.code(), ErrorCode::InvalidInput);
179 assert!(err.to_string().contains("too large"));
180 }
181
182 #[tokio::test]
183 async fn get_prunes_unrelated_expired_entries() {
184 let now = Arc::new(Mutex::new(Instant::now()));
185 let clock = Arc::clone(&now);
186 let cache = MemoryCache::new_with_clock(None, None, move || *clock.lock());
187 cache
188 .set("expired", "value", Some(Duration::from_secs(1)))
189 .await
190 .unwrap();
191 cache.set("live", "value", None).await.unwrap();
192 *now.lock() += Duration::from_secs(2);
193
194 assert_eq!(cache.get("live").await.unwrap().as_deref(), Some("value"));
195 assert_eq!(cache.entries.lock().len(), 1);
196 }
197
198 #[tokio::test]
199 async fn delete_prunes_unrelated_expired_entries() {
200 let now = Arc::new(Mutex::new(Instant::now()));
201 let clock = Arc::clone(&now);
202 let cache = MemoryCache::new_with_clock(None, None, move || *clock.lock());
203 cache
204 .set("expired", "value", Some(Duration::from_secs(1)))
205 .await
206 .unwrap();
207 cache.set("live", "value", None).await.unwrap();
208 *now.lock() += Duration::from_secs(2);
209
210 assert!(!cache.delete("missing").await.unwrap());
211 assert_eq!(cache.entries.lock().len(), 1);
212 }
213}