use crate::cache::CacheService;
use crate::error::StorageError;
use async_trait::async_trait;
use oxcache::Cache;
use oxcache::error::OxCacheError;
use std::sync::Arc;
use std::time::Duration;
pub struct MemoryCache {
inner: Arc<Cache<String, String>>,
}
impl MemoryCache {
pub fn new(default_ttl: Option<u64>) -> Self {
let mut builder = Cache::builder();
if let Some(ttl) = default_ttl {
builder = builder.ttl(Duration::from_secs(ttl));
}
let cache = builder
.build_sync()
.unwrap_or_else(|e| panic!("oxcache Cache build failed: {e}"));
Self {
inner: Arc::new(cache),
}
}
pub fn with_ttl(ttl_seconds: u64) -> Self {
Self::new(Some(ttl_seconds))
}
pub fn no_expiry() -> Self {
Self::new(None)
}
}
fn map_error(e: OxCacheError) -> StorageError {
match e {
OxCacheError::Connection(_) => StorageError::ConnectionError(e.to_string()),
OxCacheError::Timeout(_) => StorageError::ConnectionError(e.to_string()),
_ => StorageError::QueryError(e.to_string()),
}
}
#[async_trait]
impl CacheService for MemoryCache {
async fn get(&self, key: &str) -> Result<Option<String>, StorageError> {
self.inner.get(&key.to_string()).await.map_err(map_error)
}
async fn set(&self, key: &str, value: &str, ttl: Option<u64>) -> Result<(), StorageError> {
self.inner
.set_with_ttl(
&key.to_string(),
&value.to_string(),
ttl.map(Duration::from_secs),
)
.await
.map_err(map_error)
}
async fn delete(&self, key: &str) -> Result<(), StorageError> {
self.inner.delete(&key.to_string()).await.map_err(map_error)
}
async fn set_with_ttl(
&self,
key: &str,
value: &str,
ttl: Duration,
) -> Result<(), StorageError> {
self.inner
.set_with_ttl(&key.to_string(), &value.to_string(), Some(ttl))
.await
.map_err(map_error)
}
}
impl Default for MemoryCache {
fn default() -> Self {
Self::new(Some(300)) }
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::{Duration, sleep};
#[tokio::test]
async fn test_memory_cache_basic_operations() {
let cache = MemoryCache::default();
cache.set("key1", "value1", None).await.unwrap();
let result = cache.get("key1").await.unwrap();
assert_eq!(result, Some("value1".to_string()));
cache.delete("key1").await.unwrap();
let result = cache.get("key1").await.unwrap();
assert_eq!(result, None);
}
#[tokio::test]
async fn test_memory_cache_with_ttl() {
let cache = MemoryCache::new(Some(1));
cache
.set("expiring_key", "expiring_value", None)
.await
.unwrap();
let result = cache.get("expiring_key").await.unwrap();
assert_eq!(result, Some("expiring_value".to_string()));
sleep(Duration::from_secs(2)).await;
let result = cache.get("expiring_key").await.unwrap();
assert_eq!(result, None);
}
#[tokio::test]
async fn test_memory_cache_set_with_specific_ttl() {
let cache = MemoryCache::default();
cache
.set_with_ttl("specific_key", "specific_value", Duration::from_millis(500))
.await
.unwrap();
sleep(Duration::from_millis(400)).await;
let result = cache.get("specific_key").await.unwrap();
assert_eq!(result, Some("specific_value".to_string()));
sleep(Duration::from_millis(300)).await;
let result = cache.get("specific_key").await.unwrap();
assert_eq!(
result, None,
"a 500ms TTL must expire precisely after 700ms"
);
}
#[tokio::test]
#[ignore = "wall-clock 301s; run explicitly for the full-duration default TTL anchor"]
async fn test_default_ttl_300s_full_duration() {
let cache = MemoryCache::default();
cache.set("k", "v", None).await.unwrap();
sleep(Duration::from_secs(299)).await;
assert_eq!(
cache.get("k").await.unwrap(),
Some("v".to_string()),
"Default 300s TTL entry must stay alive at 299s"
);
sleep(Duration::from_secs(3)).await;
assert_eq!(
cache.get("k").await.unwrap(),
None,
"Default 300s TTL entry must expire at 302s"
);
}
#[tokio::test]
async fn test_memory_cache_no_expiry() {
let cache = MemoryCache::no_expiry();
cache
.set("permanent_key", "permanent_value", None)
.await
.unwrap();
let result = cache.get("permanent_key").await.unwrap();
assert_eq!(result, Some("permanent_value".to_string()));
tokio::time::sleep(Duration::from_millis(100)).await;
let result = cache.get("permanent_key").await.unwrap();
assert_eq!(result, Some("permanent_value".to_string()));
}
#[tokio::test]
async fn test_default_ttl_keeps_entry_alive() {
let cache = MemoryCache::default();
cache.set("k", "v", None).await.unwrap();
sleep(Duration::from_millis(100)).await;
assert_eq!(cache.get("k").await.unwrap(), Some("v".to_string()));
}
#[tokio::test]
async fn test_per_op_ttl_overrides_long_default() {
let cache = MemoryCache::new(Some(300));
cache.set("k", "v", Some(1)).await.unwrap();
let result = cache.get("k").await.unwrap();
assert_eq!(result, Some("v".to_string()));
sleep(Duration::from_secs(2)).await;
let result = cache.get("k").await.unwrap();
assert_eq!(result, None);
}
}