use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Duration;
use chrono::{DateTime, Utc};
use rainier_support::{BoxFuture, Result};
use crate::cache::{decode_counter, encode_counter, Cache};
pub struct MemoryCache {
entries: Mutex<HashMap<String, Entry>>,
}
#[derive(Debug, Clone)]
struct Entry {
value: Vec<u8>,
expires_at: Option<DateTime<Utc>>,
}
impl Entry {
fn is_live(&self) -> bool {
match self.expires_at {
Some(at) => at > Utc::now(),
None => true,
}
}
}
impl Default for MemoryCache {
fn default() -> Self {
Self::new()
}
}
impl MemoryCache {
pub fn new() -> Self {
Self { entries: Mutex::new(HashMap::new()) }
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Entry>> {
self.entries.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn len(&self) -> usize {
self.lock().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn purge(&self) -> u64 {
let mut entries = self.lock();
let before = entries.len();
entries.retain(|_, entry| entry.is_live());
(before - entries.len()) as u64
}
fn expiry(ttl: Option<Duration>) -> Option<DateTime<Utc>> {
ttl.and_then(|ttl| chrono::Duration::from_std(ttl).ok()).map(|ttl| Utc::now() + ttl)
}
}
impl Cache for MemoryCache {
fn name(&self) -> &str {
"memory"
}
fn is_shared(&self) -> bool {
false
}
fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>> {
Box::pin(async move {
let mut entries = self.lock();
match entries.get(key) {
Some(entry) if entry.is_live() => Ok(Some(entry.value.clone())),
Some(_) => {
entries.remove(key);
Ok(None)
}
None => Ok(None),
}
})
}
fn put<'a>(
&'a self,
key: &'a str,
value: &'a [u8],
ttl: Option<Duration>,
) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.lock().insert(
key.to_string(),
Entry { value: value.to_vec(), expires_at: Self::expiry(ttl) },
);
Ok(())
})
}
fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
Box::pin(async move { Ok(self.lock().remove(key).is_some()) })
}
fn flush(&self) -> BoxFuture<'_, Result<()>> {
Box::pin(async move {
self.lock().clear();
Ok(())
})
}
fn add<'a>(
&'a self,
key: &'a str,
value: &'a [u8],
ttl: Option<Duration>,
) -> BoxFuture<'a, Result<bool>> {
Box::pin(async move {
let mut entries = self.lock();
if entries.get(key).is_some_and(Entry::is_live) {
return Ok(false);
}
entries.insert(
key.to_string(),
Entry { value: value.to_vec(), expires_at: Self::expiry(ttl) },
);
Ok(true)
})
}
fn forget_if<'a>(&'a self, key: &'a str, expected: &'a [u8]) -> BoxFuture<'a, Result<bool>> {
Box::pin(async move {
let mut entries = self.lock();
match entries.get(key) {
Some(entry) if entry.is_live() && entry.value == expected => {
entries.remove(key);
Ok(true)
}
_ => Ok(false),
}
})
}
fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>> {
Box::pin(async move {
let mut entries = self.lock();
let current = match entries.get(key) {
Some(entry) if entry.is_live() => decode_counter(&entry.value)?,
_ => 0,
};
let next = current.saturating_add(by);
let expires_at = entries.get(key).filter(|e| e.is_live()).and_then(|e| e.expires_at);
entries.insert(key.to_string(), Entry { value: encode_counter(next), expires_at });
Ok(next)
})
}
}
impl std::fmt::Debug for MemoryCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemoryCache").field("entries", &self.len()).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::CacheExt;
#[tokio::test]
async fn a_value_round_trips() {
let cache = MemoryCache::new();
cache.put("k", b"v", None).await.unwrap();
assert_eq!(cache.get("k").await.unwrap(), Some(b"v".to_vec()));
assert!(cache.has("k").await.unwrap());
}
#[tokio::test]
async fn a_miss_is_none_not_an_error() {
assert_eq!(MemoryCache::new().get("absent").await.unwrap(), None);
assert!(!MemoryCache::new().has("absent").await.unwrap());
}
#[tokio::test]
async fn forgetting_reports_whether_it_was_there() {
let cache = MemoryCache::new();
cache.put("k", b"v", None).await.unwrap();
assert!(cache.forget("k").await.unwrap());
assert!(!cache.forget("k").await.unwrap());
}
#[tokio::test]
async fn an_expired_value_reads_as_absent_and_is_dropped() {
let cache = MemoryCache::new();
cache.put("k", b"v", Some(Duration::from_nanos(1))).await.unwrap();
assert_eq!(cache.get("k").await.unwrap(), None);
assert!(cache.is_empty(), "reading an expired key should clean it up");
}
#[tokio::test]
async fn no_ttl_means_no_expiry() {
let cache = MemoryCache::new();
cache.put("k", b"v", None).await.unwrap();
assert_eq!(cache.purge(), 0);
assert!(cache.get("k").await.unwrap().is_some());
}
#[tokio::test]
async fn purge_removes_only_the_expired() {
let cache = MemoryCache::new();
cache.put("gone", b"v", Some(Duration::from_nanos(1))).await.unwrap();
cache.put("kept", b"v", None).await.unwrap();
assert_eq!(cache.purge(), 1);
assert_eq!(cache.len(), 1);
assert!(cache.get("kept").await.unwrap().is_some());
}
#[tokio::test]
async fn flush_empties_everything() {
let cache = MemoryCache::new();
cache.put("a", b"1", None).await.unwrap();
cache.put("b", b"2", None).await.unwrap();
cache.flush().await.unwrap();
assert!(cache.is_empty());
}
#[tokio::test]
async fn increment_starts_from_zero() {
let cache = MemoryCache::new();
assert_eq!(cache.increment("hits", 1).await.unwrap(), 1);
assert_eq!(cache.increment("hits", 1).await.unwrap(), 2);
assert_eq!(cache.increment("hits", 5).await.unwrap(), 7);
assert_eq!(cache.decrement("hits", 3).await.unwrap(), 4);
}
#[tokio::test]
async fn increment_does_not_extend_the_window() {
let cache = MemoryCache::new();
cache.put("hits", b"0", Some(Duration::from_secs(60))).await.unwrap();
let before = cache.lock().get("hits").unwrap().expires_at;
cache.increment("hits", 1).await.unwrap();
assert_eq!(cache.lock().get("hits").unwrap().expires_at, before);
}
#[tokio::test]
async fn increment_saturates_rather_than_wrapping() {
let cache = MemoryCache::new();
cache.put("k", i64::MAX.to_string().as_bytes(), None).await.unwrap();
assert_eq!(cache.increment("k", 1).await.unwrap(), i64::MAX);
}
#[tokio::test]
async fn incrementing_a_non_counter_errors_rather_than_overwriting() {
let cache = MemoryCache::new();
cache.put("name", b"Ada", None).await.unwrap();
assert!(cache.increment("name", 1).await.is_err());
assert_eq!(cache.get("name").await.unwrap(), Some(b"Ada".to_vec()));
}
#[tokio::test]
async fn concurrent_increments_do_not_lose_any() {
let cache = std::sync::Arc::new(MemoryCache::new());
let mut handles = Vec::new();
for _ in 0..50 {
let cache = std::sync::Arc::clone(&cache);
handles.push(tokio::spawn(async move { cache.increment("hits", 1).await }));
}
for handle in handles {
handle.await.unwrap().unwrap();
}
assert_eq!(cache.get_string("hits").await.unwrap().as_deref(), Some("50"));
}
#[tokio::test]
async fn json_round_trips() {
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
struct Post {
id: u64,
title: String,
}
let cache = MemoryCache::new();
let post = Post { id: 1, title: "Hello".into() };
cache.put_json("post", &post, None).await.unwrap();
assert_eq!(cache.get_json::<Post>("post").await.unwrap(), Some(post));
}
#[tokio::test]
async fn an_unreadable_cached_value_reads_as_a_miss() {
let cache = MemoryCache::new();
cache.put("post", b"not json at all", None).await.unwrap();
assert_eq!(cache.get_json::<Vec<u8>>("post").await.unwrap(), None);
}
#[tokio::test]
async fn strings_round_trip() {
let cache = MemoryCache::new();
cache.put_string("name", "Ada", None).await.unwrap();
assert_eq!(cache.get_string("name").await.unwrap().as_deref(), Some("Ada"));
}
#[tokio::test]
async fn add_only_stores_when_absent() {
let cache = MemoryCache::new();
assert!(cache.add("k", b"first", None).await.unwrap());
assert!(!cache.add("k", b"second", None).await.unwrap());
assert_eq!(cache.get("k").await.unwrap(), Some(b"first".to_vec()));
}
#[tokio::test]
async fn the_port_is_object_safe() {
let cache: std::sync::Arc<dyn Cache> = std::sync::Arc::new(MemoryCache::new());
cache.put("k", b"v", None).await.unwrap();
assert_eq!(cache.get_string("k").await.unwrap().as_deref(), Some("v"));
}
}