1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use serde_json::Value;
5
6use crate::schema::HaysonRecord;
7
8pub const DEFAULT_LARGE_BYTES: usize = 4 * 1024;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum CachePolicy {
21 #[default]
22 Disabled,
23 Enabled {
24 entries: usize,
26 large_entries: usize,
29 large_bytes: usize,
31 },
32}
33
34impl CachePolicy {
35 pub fn with_entries(entries: usize) -> Self {
39 if entries == 0 {
40 return CachePolicy::Disabled;
41 }
42 CachePolicy::Enabled {
43 entries,
44 large_entries: (entries / 8).max(1),
45 large_bytes: DEFAULT_LARGE_BYTES,
46 }
47 }
48}
49
50fn approx_bytes(record: &HaysonRecord) -> usize {
55 record
56 .iter()
57 .map(|(field, value)| field.len() + value_bytes(value))
58 .sum()
59}
60
61fn value_bytes(value: &Value) -> usize {
62 match value {
63 Value::Null | Value::Bool(_) | Value::Number(_) => 8,
64 Value::String(text) => text.len(),
65 Value::Array(items) => items.iter().map(value_bytes).sum::<usize>() + 8,
66 Value::Object(fields) => {
67 fields
68 .iter()
69 .map(|(field, value)| field.len() + value_bytes(value))
70 .sum::<usize>()
71 + 8
72 }
73 }
74}
75
76struct Node {
77 key: Arc<str>,
78 record: HaysonRecord,
79 prev: Option<usize>,
80 next: Option<usize>,
81}
82
83struct Lru {
90 capacity: usize,
91 map: HashMap<Arc<str>, usize>,
92 nodes: Vec<Node>,
93 free: Vec<usize>,
94 head: Option<usize>,
95 tail: Option<usize>,
96}
97
98impl Lru {
99 fn new(capacity: usize) -> Self {
100 Lru {
101 capacity,
102 map: HashMap::new(),
103 nodes: Vec::new(),
104 free: Vec::new(),
105 head: None,
106 tail: None,
107 }
108 }
109
110 fn get(&mut self, key: &str) -> Option<&HaysonRecord> {
111 let at = *self.map.get(key)?;
112 self.unlink(at);
113 self.push_front(at);
114 Some(&self.nodes[at].record)
115 }
116
117 fn insert(&mut self, key: &str, record: HaysonRecord) {
118 if self.capacity == 0 {
119 return;
120 }
121 if let Some(&at) = self.map.get(key) {
122 self.nodes[at].record = record;
123 self.unlink(at);
124 self.push_front(at);
125 return;
126 }
127 if self.map.len() >= self.capacity {
128 self.evict_tail();
129 }
130 let key: Arc<str> = Arc::from(key);
131 let node = Node {
132 key: Arc::clone(&key),
133 record,
134 prev: None,
135 next: None,
136 };
137 let at = match self.free.pop() {
138 Some(at) => {
139 self.nodes[at] = node;
140 at
141 }
142 None => {
143 self.nodes.push(node);
144 self.nodes.len() - 1
145 }
146 };
147 self.map.insert(key, at);
148 self.push_front(at);
149 }
150
151 fn remove(&mut self, key: &str) {
152 let Some(at) = self.map.remove(key) else { return };
153 self.unlink(at);
154 self.release(at);
155 }
156
157 fn clear(&mut self) {
158 self.map.clear();
159 self.nodes.clear();
160 self.free.clear();
161 self.head = None;
162 self.tail = None;
163 }
164
165 #[cfg(test)]
166 fn len(&self) -> usize {
167 self.map.len()
168 }
169
170 fn evict_tail(&mut self) {
171 let Some(at) = self.tail else { return };
172 let key = Arc::clone(&self.nodes[at].key);
173 self.map.remove(&key);
174 self.unlink(at);
175 self.release(at);
176 }
177
178 fn release(&mut self, at: usize) {
181 self.nodes[at].key = Arc::from("");
182 self.nodes[at].record = HaysonRecord::new();
183 self.free.push(at);
184 }
185
186 fn unlink(&mut self, at: usize) {
188 let prev = self.nodes[at].prev.take();
189 let next = self.nodes[at].next.take();
190 match prev {
191 Some(prev) => self.nodes[prev].next = next,
192 None => self.head = next,
193 }
194 match next {
195 Some(next) => self.nodes[next].prev = prev,
196 None => self.tail = prev,
197 }
198 }
199
200 fn push_front(&mut self, at: usize) {
201 self.nodes[at].prev = None;
202 self.nodes[at].next = self.head;
203 if let Some(head) = self.head {
204 self.nodes[head].prev = Some(at);
205 }
206 self.head = Some(at);
207 if self.tail.is_none() {
208 self.tail = Some(at);
209 }
210 }
211}
212
213struct Tiers {
214 small: Lru,
215 large: Lru,
216}
217
218pub(crate) struct RecordCache {
227 policy: CachePolicy,
228 tiers: Mutex<Tiers>,
229}
230
231impl RecordCache {
232 pub(crate) fn new(policy: CachePolicy) -> Self {
233 let (entries, large_entries) = match policy {
234 CachePolicy::Disabled => (0, 0),
235 CachePolicy::Enabled {
236 entries, large_entries, ..
237 } => (entries, large_entries),
238 };
239 RecordCache {
240 policy,
241 tiers: Mutex::new(Tiers {
242 small: Lru::new(entries),
243 large: Lru::new(large_entries),
244 }),
245 }
246 }
247
248 pub(crate) fn policy(&self) -> CachePolicy {
249 self.policy
250 }
251
252 fn enabled(&self) -> bool {
253 self.policy != CachePolicy::Disabled
254 }
255
256 pub(crate) fn get(&self, key: &str) -> Option<HaysonRecord> {
260 if !self.enabled() {
261 return None;
262 }
263 let mut tiers = self.locked();
264 if let Some(record) = tiers.small.get(key) {
265 return Some(record.clone());
266 }
267 tiers.large.get(key).cloned()
268 }
269
270 pub(crate) fn insert(&self, key: &str, record: &HaysonRecord) {
271 let CachePolicy::Enabled { large_bytes, .. } = self.policy else {
272 return;
273 };
274 let large = approx_bytes(record) >= large_bytes;
275 let mut tiers = self.locked();
276 let tier = if large { &mut tiers.large } else { &mut tiers.small };
277 tier.insert(key, record.clone());
278 }
279
280 pub(crate) fn invalidate(&self, key: &str) {
281 if !self.enabled() {
282 return;
283 }
284 let mut tiers = self.locked();
285 tiers.small.remove(key);
286 tiers.large.remove(key);
287 }
288
289 pub(crate) fn clear(&self) {
290 if !self.enabled() {
291 return;
292 }
293 let mut tiers = self.locked();
294 tiers.small.clear();
295 tiers.large.clear();
296 }
297
298 #[cfg(test)]
299 pub(crate) fn len(&self) -> (usize, usize) {
300 let tiers = self.locked();
301 (tiers.small.len(), tiers.large.len())
302 }
303
304 fn locked(&self) -> std::sync::MutexGuard<'_, Tiers> {
306 self.tiers.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use serde_json::json;
313
314 use super::*;
315
316 fn record(name: &str) -> HaysonRecord {
317 let mut record = HaysonRecord::new();
318 record.insert("name".to_string(), json!(name));
319 record
320 }
321
322 fn big(bytes: usize) -> HaysonRecord {
323 let mut record = HaysonRecord::new();
324 record.insert("body".to_string(), json!("x".repeat(bytes)));
325 record
326 }
327
328 fn cache_of(entries: usize, large_entries: usize) -> RecordCache {
329 RecordCache::new(CachePolicy::Enabled {
330 entries,
331 large_entries,
332 large_bytes: DEFAULT_LARGE_BYTES,
333 })
334 }
335
336 #[test]
337 fn a_disabled_cache_answers_nothing_and_keeps_nothing() {
338 let cache = RecordCache::new(CachePolicy::Disabled);
339 cache.insert("u1", &record("Ania"));
340 assert_eq!(cache.get("u1"), None);
341 assert_eq!(cache.len(), (0, 0));
342 }
343
344 #[test]
345 fn with_entries_derives_a_smaller_large_tier_and_reads_zero_as_disabled() {
346 assert_eq!(CachePolicy::with_entries(0), CachePolicy::Disabled);
347 assert_eq!(
348 CachePolicy::with_entries(64),
349 CachePolicy::Enabled {
350 entries: 64,
351 large_entries: 8,
352 large_bytes: DEFAULT_LARGE_BYTES,
353 }
354 );
355 assert_eq!(
356 CachePolicy::with_entries(1),
357 CachePolicy::Enabled {
358 entries: 1,
359 large_entries: 1,
360 large_bytes: DEFAULT_LARGE_BYTES,
361 }
362 );
363 }
364
365 #[test]
366 fn what_was_inserted_reads_back_until_it_is_invalidated() {
367 let cache = cache_of(4, 2);
368 cache.insert("u1", &record("Ania"));
369 assert_eq!(cache.get("u1"), Some(record("Ania")));
370 cache.invalidate("u1");
371 assert_eq!(cache.get("u1"), None);
372 }
373
374 #[test]
375 fn a_second_insert_on_one_key_replaces_the_document_without_taking_a_second_slot() {
376 let cache = cache_of(4, 2);
377 cache.insert("u1", &record("Ania"));
378 cache.insert("u1", &record("Zosia"));
379 assert_eq!(cache.get("u1"), Some(record("Zosia")));
380 assert_eq!(cache.len(), (1, 0));
381 }
382
383 #[test]
384 fn a_full_tier_evicts_the_least_recently_read_key_and_keeps_the_one_just_read() {
385 let cache = cache_of(2, 1);
386 cache.insert("u1", &record("Ania"));
387 cache.insert("u2", &record("Piotr"));
388 assert_eq!(cache.get("u1"), Some(record("Ania")));
390 cache.insert("u3", &record("Zosia"));
391
392 assert_eq!(cache.get("u2"), None);
393 assert_eq!(cache.get("u1"), Some(record("Ania")));
394 assert_eq!(cache.get("u3"), Some(record("Zosia")));
395 assert_eq!(cache.len(), (2, 0));
396 }
397
398 #[test]
399 fn a_large_document_lands_in_its_own_tier_and_evicts_only_large_ones() {
400 let cache = cache_of(2, 1);
401 cache.insert("u1", &record("Ania"));
402 cache.insert("d1", &big(DEFAULT_LARGE_BYTES));
403 cache.insert("d2", &big(DEFAULT_LARGE_BYTES));
404
405 assert_eq!(cache.len(), (1, 1));
406 assert_eq!(cache.get("d1"), None);
407 assert_eq!(cache.get("d2"), Some(big(DEFAULT_LARGE_BYTES)));
408 assert_eq!(cache.get("u1"), Some(record("Ania")));
409 }
410
411 #[test]
412 fn a_document_one_byte_under_the_line_is_still_a_small_one() {
413 let cache = cache_of(2, 1);
414 cache.insert("d1", &big(DEFAULT_LARGE_BYTES - "body".len() - 1));
415 assert_eq!(cache.len(), (1, 0));
416 }
417
418 #[test]
419 fn clear_empties_both_tiers() {
420 let cache = cache_of(2, 1);
421 cache.insert("u1", &record("Ania"));
422 cache.insert("d1", &big(DEFAULT_LARGE_BYTES));
423 cache.clear();
424 assert_eq!(cache.len(), (0, 0));
425 assert_eq!(cache.get("u1"), None);
426 assert_eq!(cache.get("d1"), None);
427 }
428
429 #[test]
430 fn invalidating_a_key_nobody_cached_changes_nothing() {
431 let cache = cache_of(2, 1);
432 cache.insert("u1", &record("Ania"));
433 cache.invalidate("u404");
434 assert_eq!(cache.len(), (1, 0));
435 assert_eq!(cache.get("u1"), Some(record("Ania")));
436 }
437
438 #[test]
439 fn a_tier_keeps_its_capacity_across_eviction_and_reinsertion() {
440 let cache = cache_of(2, 1);
441 for round in 0..10 {
442 cache.insert(&format!("u{round}"), &record("Ania"));
443 assert!(cache.len().0 <= 2);
444 }
445 assert_eq!(cache.len(), (2, 0));
446 assert_eq!(cache.get("u9"), Some(record("Ania")));
447 assert_eq!(cache.get("u8"), Some(record("Ania")));
448 assert_eq!(cache.get("u7"), None);
449 }
450
451 #[test]
452 fn approx_bytes_counts_strings_for_real_and_nested_values_too() {
453 let mut record = HaysonRecord::new();
454 record.insert("id".to_string(), json!("u1"));
455 record.insert("tags".to_string(), json!(["aaa", "bbb"]));
456 record.insert("meta".to_string(), json!({ "note": "hello" }));
457 assert_eq!(approx_bytes(&record), 2 + 2 + 4 + (3 + 3 + 8) + 4 + (4 + 5 + 8));
458 }
459}