Skip to main content

caelix_core/
cache.rs

1use crate::{
2    BoxFuture, Container, Injectable, InternalServerErrorException, Module, ModuleMetadata,
3    PayloadTooLargeException,
4};
5use serde::{Serialize, de::DeserializeOwned};
6use serde_json::Value;
7use std::{
8    collections::HashMap,
9    sync::{Arc, RwLock},
10    time::{Duration, Instant},
11};
12
13const DEFAULT_MAX_ENTRIES: usize = 1024;
14const DEFAULT_MAX_VALUE_BYTES: usize = 1024 * 1024;
15
16pub trait CacheStore: Send + Sync + 'static {
17    fn get(&self, key: String) -> BoxFuture<'_, crate::Result<Option<Value>>>;
18    fn set(
19        &self,
20        key: String,
21        value: Value,
22        ttl: Option<Duration>,
23    ) -> BoxFuture<'_, crate::Result<()>>;
24    fn delete(&self, key: String) -> BoxFuture<'_, crate::Result<()>>;
25    fn clear(&self) -> BoxFuture<'_, crate::Result<()>>;
26}
27
28#[derive(Clone)]
29struct CacheEntry {
30    value: Value,
31    inserted_at: Instant,
32    expires_at: Option<Instant>,
33}
34
35impl CacheEntry {
36    fn new(value: Value, ttl: Option<Duration>) -> Self {
37        let now = Instant::now();
38        Self {
39            value,
40            inserted_at: now,
41            expires_at: ttl.and_then(|ttl| now.checked_add(ttl)),
42        }
43    }
44
45    fn is_expired(&self) -> bool {
46        self.expires_at
47            .is_some_and(|expires_at| Instant::now() >= expires_at)
48    }
49}
50
51#[derive(Clone, Debug)]
52pub struct MemoryCacheOptions {
53    pub max_entries: usize,
54    pub max_value_bytes: usize,
55    pub default_ttl: Option<Duration>,
56}
57
58impl Default for MemoryCacheOptions {
59    fn default() -> Self {
60        Self {
61            max_entries: DEFAULT_MAX_ENTRIES,
62            max_value_bytes: DEFAULT_MAX_VALUE_BYTES,
63            default_ttl: None,
64        }
65    }
66}
67
68pub struct MemoryCacheStore {
69    options: MemoryCacheOptions,
70    entries: RwLock<HashMap<String, CacheEntry>>,
71}
72
73impl MemoryCacheStore {
74    pub fn new() -> Self {
75        Self::with_options(MemoryCacheOptions::default())
76    }
77
78    pub fn with_options(options: MemoryCacheOptions) -> Self {
79        Self {
80            options,
81            entries: RwLock::new(HashMap::new()),
82        }
83    }
84
85    pub fn len(&self) -> usize {
86        self.entries.read().map_or(0, |entries| entries.len())
87    }
88
89    pub fn is_empty(&self) -> bool {
90        self.len() == 0
91    }
92
93    fn remove_expired(entries: &mut HashMap<String, CacheEntry>) {
94        entries.retain(|_, entry| !entry.is_expired());
95    }
96
97    fn evict_to_capacity(&self, entries: &mut HashMap<String, CacheEntry>) {
98        if self.options.max_entries == 0 {
99            entries.clear();
100            return;
101        }
102
103        while entries.len() > self.options.max_entries {
104            let Some(oldest_key) = entries
105                .iter()
106                .min_by_key(|(_, entry)| entry.inserted_at)
107                .map(|(key, _)| key.clone())
108            else {
109                return;
110            };
111
112            entries.remove(&oldest_key);
113        }
114    }
115}
116
117impl Default for MemoryCacheStore {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123impl Injectable for MemoryCacheStore {
124    fn dependencies() -> Vec<crate::ProviderDependency> {
125        crate::provider_dependencies![]
126    }
127
128    fn create(_container: &Container) -> BoxFuture<'_, crate::Result<Self>> {
129        Box::pin(async { Ok(Self::new()) })
130    }
131}
132
133impl CacheStore for MemoryCacheStore {
134    fn get(&self, key: String) -> BoxFuture<'_, crate::Result<Option<Value>>> {
135        Box::pin(async move {
136            let mut entries = self.entries.write().map_err(|_| {
137                InternalServerErrorException::new(anyhow::anyhow!("cache store lock poisoned"))
138            })?;
139            let Some(entry) = entries.get(&key) else {
140                return Ok(None);
141            };
142
143            if entry.is_expired() {
144                entries.remove(&key);
145                return Ok(None);
146            }
147
148            Ok(Some(entry.value.clone()))
149        })
150    }
151
152    fn set(
153        &self,
154        key: String,
155        value: Value,
156        ttl: Option<Duration>,
157    ) -> BoxFuture<'_, crate::Result<()>> {
158        Box::pin(async move {
159            let value_size = serde_json::to_vec(&value)
160                .map_err(InternalServerErrorException::new)?
161                .len();
162
163            if value_size > self.options.max_value_bytes {
164                return Err(PayloadTooLargeException::new(format!(
165                    "cache value exceeds the configured limit of {} bytes",
166                    self.options.max_value_bytes
167                )));
168            }
169
170            let ttl = ttl.or(self.options.default_ttl);
171            let mut entries = self.entries.write().map_err(|_| {
172                InternalServerErrorException::new(anyhow::anyhow!("cache store lock poisoned"))
173            })?;
174            Self::remove_expired(&mut entries);
175            entries.insert(key, CacheEntry::new(value, ttl));
176            self.evict_to_capacity(&mut entries);
177            Ok(())
178        })
179    }
180
181    fn delete(&self, key: String) -> BoxFuture<'_, crate::Result<()>> {
182        Box::pin(async move {
183            self.entries
184                .write()
185                .map_err(|_| {
186                    InternalServerErrorException::new(anyhow::anyhow!("cache store lock poisoned"))
187                })?
188                .remove(&key);
189            Ok(())
190        })
191    }
192
193    fn clear(&self) -> BoxFuture<'_, crate::Result<()>> {
194        Box::pin(async move {
195            self.entries
196                .write()
197                .map_err(|_| {
198                    InternalServerErrorException::new(anyhow::anyhow!("cache store lock poisoned"))
199                })?
200                .clear();
201            Ok(())
202        })
203    }
204}
205
206pub struct Cache {
207    store: Arc<dyn CacheStore>,
208}
209
210impl Cache {
211    pub fn new(store: Arc<dyn CacheStore>) -> Self {
212        Self { store }
213    }
214
215    pub async fn get<T>(&self, key: impl Into<String>) -> crate::Result<Option<T>>
216    where
217        T: DeserializeOwned,
218    {
219        let Some(value) = self.store.get(key.into()).await? else {
220            return Ok(None);
221        };
222
223        serde_json::from_value(value)
224            .map(Some)
225            .map_err(InternalServerErrorException::new)
226    }
227
228    pub async fn set<T>(&self, key: impl Into<String>, value: T) -> crate::Result<()>
229    where
230        T: Serialize,
231    {
232        self.set_with_optional_ttl(key, value, None).await
233    }
234
235    pub async fn set_with_ttl<T>(
236        &self,
237        key: impl Into<String>,
238        value: T,
239        ttl: Duration,
240    ) -> crate::Result<()>
241    where
242        T: Serialize,
243    {
244        self.set_with_optional_ttl(key, value, Some(ttl)).await
245    }
246
247    pub async fn set_with_optional_ttl<T>(
248        &self,
249        key: impl Into<String>,
250        value: T,
251        ttl: Option<Duration>,
252    ) -> crate::Result<()>
253    where
254        T: Serialize,
255    {
256        let value = serde_json::to_value(value).map_err(InternalServerErrorException::new)?;
257        self.store.set(key.into(), value, ttl).await
258    }
259
260    pub async fn delete(&self, key: impl Into<String>) -> crate::Result<()> {
261        self.store.delete(key.into()).await
262    }
263
264    pub async fn clear(&self) -> crate::Result<()> {
265        self.store.clear().await
266    }
267}
268
269impl Injectable for Cache {
270    fn dependencies() -> Vec<crate::ProviderDependency> {
271        crate::provider_dependencies![MemoryCacheStore]
272    }
273
274    fn create(container: &Container) -> BoxFuture<'_, crate::Result<Self>> {
275        Box::pin(async move {
276            let store = container.resolve::<MemoryCacheStore>()? as Arc<dyn CacheStore>;
277            Ok(Self::new(store))
278        })
279    }
280}
281
282pub struct CacheModule;
283
284impl Module for CacheModule {
285    fn register() -> ModuleMetadata {
286        ModuleMetadata::new()
287            .provider::<MemoryCacheStore>()
288            .provider::<Cache>()
289            .export::<MemoryCacheStore>()
290            .export::<Cache>()
291    }
292}