use super::Cache;
use async_trait::async_trait;
use reinhardt_core::exception::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone)]
pub struct HybridCache<L1, L2>
where
L1: Cache + Clone,
L2: Cache + Clone,
{
l1: Arc<L1>,
l2: Arc<L2>,
}
impl<L1, L2> HybridCache<L1, L2>
where
L1: Cache + Clone,
L2: Cache + Clone,
{
pub fn new(l1: L1, l2: L2) -> Self {
Self {
l1: Arc::new(l1),
l2: Arc::new(l2),
}
}
pub fn l1(&self) -> &L1 {
&self.l1
}
pub fn l2(&self) -> &L2 {
&self.l2
}
#[allow(dead_code)]
async fn promote<T>(&self, key: &str, value: &T, ttl: Option<Duration>) -> Result<()>
where
T: Serialize + Send + Sync,
{
self.l1.set(key, value, ttl).await
}
}
#[async_trait]
impl<L1, L2> Cache for HybridCache<L1, L2>
where
L1: Cache + Clone + 'static,
L2: Cache + Clone + 'static,
{
async fn get<T>(&self, key: &str) -> Result<Option<T>>
where
T: for<'de> Deserialize<'de> + Serialize + Send + Sync,
{
if let Some(value) = self.l1.get::<T>(key).await? {
return Ok(Some(value));
}
if let Some(value) = self.l2.get::<T>(key).await? {
self.l1.set(key, &value, None).await?;
return Ok(Some(value));
}
Ok(None)
}
async fn set<T>(&self, key: &str, value: &T, ttl: Option<Duration>) -> Result<()>
where
T: Serialize + Send + Sync,
{
self.l1.set(key, value, ttl).await?;
self.l2.set(key, value, ttl).await?;
Ok(())
}
async fn delete(&self, key: &str) -> Result<()> {
self.l1.delete(key).await?;
self.l2.delete(key).await?;
Ok(())
}
async fn has_key(&self, key: &str) -> Result<bool> {
if self.l1.has_key(key).await? {
return Ok(true);
}
self.l2.has_key(key).await
}
async fn clear(&self) -> Result<()> {
self.l1.clear().await?;
self.l2.clear().await?;
Ok(())
}
async fn get_many<T>(&self, keys: &[&str]) -> Result<HashMap<String, T>>
where
T: for<'de> Deserialize<'de> + Serialize + Send + Sync,
{
let mut results = HashMap::new();
let l1_results = self.l1.get_many::<T>(keys).await?;
results.extend(l1_results);
let missing_keys: Vec<&str> = keys
.iter()
.filter(|k| !results.contains_key(**k))
.copied()
.collect();
if !missing_keys.is_empty() {
let l2_results = self.l2.get_many::<T>(&missing_keys).await?;
for (key, value) in &l2_results {
self.l1.set(key, value, None).await?;
}
results.extend(l2_results);
}
Ok(results)
}
async fn set_many<T>(&self, values: HashMap<String, T>, ttl: Option<Duration>) -> Result<()>
where
T: Serialize + Send + Sync,
{
for (key, value) in values.iter() {
self.l1.set(key, value, ttl).await?;
self.l2.set(key, value, ttl).await?;
}
Ok(())
}
async fn delete_many(&self, keys: &[&str]) -> Result<()> {
self.l1.delete_many(keys).await?;
self.l2.delete_many(keys).await?;
Ok(())
}
async fn incr(&self, key: &str, delta: i64) -> Result<i64> {
let result = self.l2.incr(key, delta).await?;
self.l1.set(key, &result, None).await?;
Ok(result)
}
async fn decr(&self, key: &str, delta: i64) -> Result<i64> {
let result = self.l2.decr(key, delta).await?;
self.l1.set(key, &result, None).await?;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::InMemoryCache;
#[tokio::test]
async fn test_hybrid_cache_l1_hit() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1, l2);
cache.set("key1", &"value1", None).await.unwrap();
let value: Option<String> = cache.get("key1").await.unwrap();
assert_eq!(value, Some("value1".to_string()));
}
#[tokio::test]
async fn test_hybrid_cache_l2_hit_promotion() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
l2.set("key1", &"value1", None).await.unwrap();
let value: Option<String> = cache.get("key1").await.unwrap();
assert_eq!(value, Some("value1".to_string()));
let l1_value: Option<String> = l1.get("key1").await.unwrap();
assert_eq!(l1_value, Some("value1".to_string()));
}
#[tokio::test]
async fn test_hybrid_cache_miss() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1, l2);
let value: Option<String> = cache.get("nonexistent").await.unwrap();
assert_eq!(value, None);
}
#[tokio::test]
async fn test_hybrid_cache_write_through() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
cache.set("key1", &"value1", None).await.unwrap();
let l1_value: Option<String> = l1.get("key1").await.unwrap();
let l2_value: Option<String> = l2.get("key1").await.unwrap();
assert_eq!(l1_value, Some("value1".to_string()));
assert_eq!(l2_value, Some("value1".to_string()));
}
#[tokio::test]
async fn test_hybrid_cache_delete_both() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
cache.set("key1", &"value1", None).await.unwrap();
cache.delete("key1").await.unwrap();
let l1_value: Option<String> = l1.get("key1").await.unwrap();
let l2_value: Option<String> = l2.get("key1").await.unwrap();
assert_eq!(l1_value, None);
assert_eq!(l2_value, None);
}
#[tokio::test]
async fn test_hybrid_cache_has_key() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
l2.set("key1", &"value1", None).await.unwrap();
assert!(cache.has_key("key1").await.unwrap());
assert!(!cache.has_key("nonexistent").await.unwrap());
}
#[tokio::test]
async fn test_hybrid_cache_clear_both() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
cache.set("key1", &"value1", None).await.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
cache.clear().await.unwrap();
let l1_value: Option<String> = l1.get("key1").await.unwrap();
let l2_value: Option<String> = l2.get("key1").await.unwrap();
assert_eq!(l1_value, None);
assert_eq!(l2_value, None);
}
#[tokio::test]
async fn test_hybrid_cache_get_many_l1_hit() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1, l2);
cache.set("key1", &"value1", None).await.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
let results: HashMap<String, String> =
cache.get_many(&["key1", "key2", "key3"]).await.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results.get("key1"), Some(&"value1".to_string()));
assert_eq!(results.get("key2"), Some(&"value2".to_string()));
}
#[tokio::test]
async fn test_hybrid_cache_get_many_l2_promotion() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
l1.set("key1", &"value1", None).await.unwrap();
l2.set("key2", &"value2", None).await.unwrap();
let results: HashMap<String, String> = cache.get_many(&["key1", "key2"]).await.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results.get("key1"), Some(&"value1".to_string()));
assert_eq!(results.get("key2"), Some(&"value2".to_string()));
let l1_value: Option<String> = l1.get("key2").await.unwrap();
assert_eq!(l1_value, Some("value2".to_string()));
}
#[tokio::test]
async fn test_hybrid_cache_set_many() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
let mut values = HashMap::new();
values.insert("key1".to_string(), "value1".to_string());
values.insert("key2".to_string(), "value2".to_string());
cache.set_many(values, None).await.unwrap();
let l1_value: Option<String> = l1.get("key1").await.unwrap();
let l2_value: Option<String> = l2.get("key1").await.unwrap();
assert_eq!(l1_value, Some("value1".to_string()));
assert_eq!(l2_value, Some("value1".to_string()));
}
#[tokio::test]
async fn test_hybrid_cache_delete_many() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
cache.set("key1", &"value1", None).await.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
cache.delete_many(&["key1", "key2"]).await.unwrap();
let l1_value: Option<String> = l1.get("key1").await.unwrap();
let l2_value: Option<String> = l2.get("key1").await.unwrap();
assert_eq!(l1_value, None);
assert_eq!(l2_value, None);
}
#[tokio::test]
async fn test_hybrid_cache_incr() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
let value = cache.incr("counter", 5).await.unwrap();
assert_eq!(value, 5);
let l1_value: Option<i64> = l1.get("counter").await.unwrap();
let l2_value: Option<i64> = l2.get("counter").await.unwrap();
assert_eq!(l1_value, Some(5));
assert_eq!(l2_value, Some(5));
}
#[tokio::test]
async fn test_hybrid_cache_decr() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
cache.set("counter", &10i64, None).await.unwrap();
let value = cache.decr("counter", 3).await.unwrap();
assert_eq!(value, 7);
let l1_value: Option<i64> = l1.get("counter").await.unwrap();
let l2_value: Option<i64> = l2.get("counter").await.unwrap();
assert_eq!(l1_value, Some(7));
assert_eq!(l2_value, Some(7));
}
#[tokio::test]
async fn test_hybrid_cache_l1_l2_access() {
let l1 = InMemoryCache::new();
let l2 = InMemoryCache::new();
let cache = HybridCache::new(l1.clone(), l2.clone());
cache.set("key1", &"value1", None).await.unwrap();
cache.l1().clear().await.unwrap();
let value: Option<String> = cache.get("key1").await.unwrap();
assert_eq!(value, Some("value1".to_string()));
let l1_value: Option<String> = cache.l1().get("key1").await.unwrap();
assert_eq!(l1_value, Some("value1".to_string()));
}
}