1use std::collections::HashMap;
7use std::sync::{Arc, Mutex, OnceLock, PoisonError};
8use std::time::{Duration, Instant};
9
10const MAX_ENTRIES: usize = 4_096;
12const MAX_ENTRY_BYTES: usize = 256 * 1024;
14const MAX_TOTAL_BYTES: usize = 32 * 1024 * 1024;
16const TTL: Duration = Duration::from_secs(3600);
18
19static GLOBAL_MEMO_CACHE: OnceLock<MemoStore> = OnceLock::new();
20
21fn get_cache() -> &'static MemoStore {
22 GLOBAL_MEMO_CACHE.get_or_init(|| {
23 MemoStore::new(Limits {
24 max_entries: MAX_ENTRIES,
25 max_entry_bytes: MAX_ENTRY_BYTES,
26 max_total_bytes: MAX_TOTAL_BYTES,
27 ttl: TTL,
28 })
29 })
30}
31
32pub fn get(key: &str) -> Option<String> {
34 get_cache().get(key).map(|value| value.to_string())
35}
36
37pub fn set(key: &str, value: &str) {
42 get_cache().set(key, value);
43}
44
45#[derive(Clone, Copy)]
46struct Limits {
47 max_entries: usize,
48 max_entry_bytes: usize,
49 max_total_bytes: usize,
50 ttl: Duration,
51}
52
53struct Entry {
54 value: Arc<str>,
55 expires_at: Instant,
56 inserted: u64,
57 bytes: usize,
58}
59
60#[derive(Default)]
61struct State {
62 entries: HashMap<Box<str>, Entry>,
63 total_bytes: usize,
64 next_insertion: u64,
65}
66
67impl State {
68 fn remove(&mut self, key: &str) {
69 if let Some(entry) = self.entries.remove(key) {
70 self.total_bytes = self.total_bytes.saturating_sub(entry.bytes);
71 }
72 }
73
74 fn retain(&mut self, keep: impl Fn(&Entry) -> bool) {
75 self.entries.retain(|_, entry| keep(entry));
76 self.total_bytes = self.entries.values().map(|entry| entry.bytes).sum();
77 }
78
79 fn fits(&self, limits: &Limits, bytes: usize) -> bool {
80 self.entries.len() < limits.max_entries
81 && self.total_bytes.saturating_add(bytes) <= limits.max_total_bytes
82 }
83
84 fn evict_oldest(&mut self, limits: &Limits, bytes: usize) {
87 let entry_target = limits.max_entries - limits.max_entries / 4;
88 let byte_target = limits.max_total_bytes - limits.max_total_bytes / 4;
89 let mut order: Vec<(u64, usize)> = self
90 .entries
91 .values()
92 .map(|entry| (entry.inserted, entry.bytes))
93 .collect();
94 order.sort_unstable();
95
96 let mut remaining_entries = self.entries.len();
97 let mut remaining_bytes = self.total_bytes;
98 let mut newest_evicted = None;
99 for (inserted, entry_bytes) in order {
100 if remaining_entries < entry_target
101 && remaining_bytes.saturating_add(bytes) <= byte_target
102 {
103 break;
104 }
105 remaining_entries -= 1;
106 remaining_bytes = remaining_bytes.saturating_sub(entry_bytes);
107 newest_evicted = Some(inserted);
108 }
109 if let Some(newest_evicted) = newest_evicted {
110 self.retain(|entry| entry.inserted > newest_evicted);
111 }
112 }
113}
114
115struct MemoStore {
116 state: Mutex<State>,
117 limits: Limits,
118}
119
120impl MemoStore {
121 fn new(limits: Limits) -> Self {
122 Self {
123 state: Mutex::new(State::default()),
124 limits,
125 }
126 }
127
128 fn state(&self) -> std::sync::MutexGuard<'_, State> {
129 self.state.lock().unwrap_or_else(PoisonError::into_inner)
132 }
133
134 fn get(&self, key: &str) -> Option<Arc<str>> {
135 let mut state = self.state();
136 let entry = state.entries.get(key)?;
137 if Instant::now() < entry.expires_at {
138 return Some(Arc::clone(&entry.value));
139 }
140 state.remove(key);
141 None
142 }
143
144 fn set(&self, key: &str, value: &str) {
145 let bytes = key.len().saturating_add(value.len());
146 let expires_at = Instant::now().checked_add(self.limits.ttl);
147 let mut state = self.state();
148 state.remove(key);
150 let Some(expires_at) = expires_at else {
151 return;
152 };
153 if bytes > self.limits.max_entry_bytes || bytes > self.limits.max_total_bytes {
154 return;
155 }
156 if !state.fits(&self.limits, bytes) {
157 let now = Instant::now();
158 state.retain(|entry| now < entry.expires_at);
159 }
160 if !state.fits(&self.limits, bytes) {
161 state.evict_oldest(&self.limits, bytes);
162 }
163 if !state.fits(&self.limits, bytes) {
164 return;
165 }
166 let inserted = state.next_insertion;
167 state.next_insertion = inserted.wrapping_add(1);
168 state.total_bytes = state.total_bytes.saturating_add(bytes);
169 state.entries.insert(
170 key.into(),
171 Entry {
172 value: value.into(),
173 expires_at,
174 inserted,
175 bytes,
176 },
177 );
178 }
179
180 #[cfg(test)]
181 fn usage(&self) -> (usize, usize) {
182 let state = self.state();
183 (state.entries.len(), state.total_bytes)
184 }
185}
186
187#[cfg(test)]
188#[allow(clippy::unwrap_used, clippy::expect_used)]
189mod tests {
190 use super::*;
191
192 fn store(max_entries: usize, max_total_bytes: usize) -> MemoStore {
193 MemoStore::new(Limits {
194 max_entries,
195 max_entry_bytes: 64,
196 max_total_bytes,
197 ttl: TTL,
198 })
199 }
200
201 #[test]
202 fn entry_count_is_bounded_and_oldest_entries_are_evicted_first() {
203 let store = store(8, 1_024);
204 for index in 0..100 {
205 store.set(&format!("key-{index}"), "value");
206 let (entries, _) = store.usage();
207 assert!(entries <= 8, "{entries} entries after insert {index}");
208 }
209 assert_eq!(store.get("key-99").as_deref(), Some("value"));
211 assert_eq!(store.get("key-0"), None);
212 }
213
214 #[test]
215 fn total_bytes_are_bounded() {
216 let store = store(1_000, 100);
217 for index in 0..50 {
218 store.set(&format!("k{index:02}"), "0123456789");
219 let (_, bytes) = store.usage();
220 assert!(bytes <= 100, "{bytes} bytes after insert {index}");
221 }
222 assert_eq!(store.get("k49").as_deref(), Some("0123456789"));
223 }
224
225 #[test]
226 fn oversized_entries_are_skipped_and_replace_stale_values() {
227 let store = store(8, 1_024);
228 store.set("key", "small");
229 store.set("key", &"x".repeat(100));
230 assert_eq!(store.get("key"), None);
231 store.set(&"k".repeat(100), "v");
232 assert_eq!(store.usage(), (0, 0));
233 }
234
235 #[test]
236 fn replacing_a_key_keeps_the_byte_count_exact() {
237 let store = store(8, 1_024);
238 store.set("key", "first");
239 store.set("key", "second value");
240 assert_eq!(store.usage(), (1, "key".len() + "second value".len()));
241 assert_eq!(store.get("key").as_deref(), Some("second value"));
242 }
243
244 #[test]
245 fn expired_entries_are_not_returned_and_are_reclaimed_before_eviction() {
246 let store = MemoStore::new(Limits {
247 max_entries: 2,
248 max_entry_bytes: 64,
249 max_total_bytes: 1_024,
250 ttl: Duration::ZERO,
251 });
252 store.set("a", "1");
253 store.set("b", "2");
254 assert_eq!(store.usage(), (2, 4));
255 store.set("c", "3");
257 assert_eq!(store.usage(), (1, 2));
258 assert_eq!(store.get("c"), None);
259 assert_eq!(store.usage(), (0, 0));
260 }
261}