use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock, PoisonError};
use std::time::{Duration, Instant};
const MAX_ENTRIES: usize = 4_096;
const MAX_ENTRY_BYTES: usize = 256 * 1024;
const MAX_TOTAL_BYTES: usize = 32 * 1024 * 1024;
const TTL: Duration = Duration::from_secs(3600);
static GLOBAL_MEMO_CACHE: OnceLock<MemoStore> = OnceLock::new();
fn get_cache() -> &'static MemoStore {
GLOBAL_MEMO_CACHE.get_or_init(|| {
MemoStore::new(Limits {
max_entries: MAX_ENTRIES,
max_entry_bytes: MAX_ENTRY_BYTES,
max_total_bytes: MAX_TOTAL_BYTES,
ttl: TTL,
})
})
}
pub fn get(key: &str) -> Option<String> {
get_cache().get(key).map(|value| value.to_string())
}
pub fn set(key: &str, value: &str) {
get_cache().set(key, value);
}
#[derive(Clone, Copy)]
struct Limits {
max_entries: usize,
max_entry_bytes: usize,
max_total_bytes: usize,
ttl: Duration,
}
struct Entry {
value: Arc<str>,
expires_at: Instant,
inserted: u64,
bytes: usize,
}
#[derive(Default)]
struct State {
entries: HashMap<Box<str>, Entry>,
total_bytes: usize,
next_insertion: u64,
}
impl State {
fn remove(&mut self, key: &str) {
if let Some(entry) = self.entries.remove(key) {
self.total_bytes = self.total_bytes.saturating_sub(entry.bytes);
}
}
fn retain(&mut self, keep: impl Fn(&Entry) -> bool) {
self.entries.retain(|_, entry| keep(entry));
self.total_bytes = self.entries.values().map(|entry| entry.bytes).sum();
}
fn fits(&self, limits: &Limits, bytes: usize) -> bool {
self.entries.len() < limits.max_entries
&& self.total_bytes.saturating_add(bytes) <= limits.max_total_bytes
}
fn evict_oldest(&mut self, limits: &Limits, bytes: usize) {
let entry_target = limits.max_entries - limits.max_entries / 4;
let byte_target = limits.max_total_bytes - limits.max_total_bytes / 4;
let mut order: Vec<(u64, usize)> = self
.entries
.values()
.map(|entry| (entry.inserted, entry.bytes))
.collect();
order.sort_unstable();
let mut remaining_entries = self.entries.len();
let mut remaining_bytes = self.total_bytes;
let mut newest_evicted = None;
for (inserted, entry_bytes) in order {
if remaining_entries < entry_target
&& remaining_bytes.saturating_add(bytes) <= byte_target
{
break;
}
remaining_entries -= 1;
remaining_bytes = remaining_bytes.saturating_sub(entry_bytes);
newest_evicted = Some(inserted);
}
if let Some(newest_evicted) = newest_evicted {
self.retain(|entry| entry.inserted > newest_evicted);
}
}
}
struct MemoStore {
state: Mutex<State>,
limits: Limits,
}
impl MemoStore {
fn new(limits: Limits) -> Self {
Self {
state: Mutex::new(State::default()),
limits,
}
}
fn state(&self) -> std::sync::MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
fn get(&self, key: &str) -> Option<Arc<str>> {
let mut state = self.state();
let entry = state.entries.get(key)?;
if Instant::now() < entry.expires_at {
return Some(Arc::clone(&entry.value));
}
state.remove(key);
None
}
fn set(&self, key: &str, value: &str) {
let bytes = key.len().saturating_add(value.len());
let expires_at = Instant::now().checked_add(self.limits.ttl);
let mut state = self.state();
state.remove(key);
let Some(expires_at) = expires_at else {
return;
};
if bytes > self.limits.max_entry_bytes || bytes > self.limits.max_total_bytes {
return;
}
if !state.fits(&self.limits, bytes) {
let now = Instant::now();
state.retain(|entry| now < entry.expires_at);
}
if !state.fits(&self.limits, bytes) {
state.evict_oldest(&self.limits, bytes);
}
if !state.fits(&self.limits, bytes) {
return;
}
let inserted = state.next_insertion;
state.next_insertion = inserted.wrapping_add(1);
state.total_bytes = state.total_bytes.saturating_add(bytes);
state.entries.insert(
key.into(),
Entry {
value: value.into(),
expires_at,
inserted,
bytes,
},
);
}
#[cfg(test)]
fn usage(&self) -> (usize, usize) {
let state = self.state();
(state.entries.len(), state.total_bytes)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn store(max_entries: usize, max_total_bytes: usize) -> MemoStore {
MemoStore::new(Limits {
max_entries,
max_entry_bytes: 64,
max_total_bytes,
ttl: TTL,
})
}
#[test]
fn entry_count_is_bounded_and_oldest_entries_are_evicted_first() {
let store = store(8, 1_024);
for index in 0..100 {
store.set(&format!("key-{index}"), "value");
let (entries, _) = store.usage();
assert!(entries <= 8, "{entries} entries after insert {index}");
}
assert_eq!(store.get("key-99").as_deref(), Some("value"));
assert_eq!(store.get("key-0"), None);
}
#[test]
fn total_bytes_are_bounded() {
let store = store(1_000, 100);
for index in 0..50 {
store.set(&format!("k{index:02}"), "0123456789");
let (_, bytes) = store.usage();
assert!(bytes <= 100, "{bytes} bytes after insert {index}");
}
assert_eq!(store.get("k49").as_deref(), Some("0123456789"));
}
#[test]
fn oversized_entries_are_skipped_and_replace_stale_values() {
let store = store(8, 1_024);
store.set("key", "small");
store.set("key", &"x".repeat(100));
assert_eq!(store.get("key"), None);
store.set(&"k".repeat(100), "v");
assert_eq!(store.usage(), (0, 0));
}
#[test]
fn replacing_a_key_keeps_the_byte_count_exact() {
let store = store(8, 1_024);
store.set("key", "first");
store.set("key", "second value");
assert_eq!(store.usage(), (1, "key".len() + "second value".len()));
assert_eq!(store.get("key").as_deref(), Some("second value"));
}
#[test]
fn expired_entries_are_not_returned_and_are_reclaimed_before_eviction() {
let store = MemoStore::new(Limits {
max_entries: 2,
max_entry_bytes: 64,
max_total_bytes: 1_024,
ttl: Duration::ZERO,
});
store.set("a", "1");
store.set("b", "2");
assert_eq!(store.usage(), (2, 4));
store.set("c", "3");
assert_eq!(store.usage(), (1, 2));
assert_eq!(store.get("c"), None);
assert_eq!(store.usage(), (0, 0));
}
}