use crate::error::{CacheError, CacheResult};
use crate::traits::CacheStore;
use futures::StreamExt;
use futures::future::join_all;
use serde::{Serialize, de::DeserializeOwned};
use std::collections::HashMap;
use std::time::Duration;
pub const DEFAULT_WARM_CONCURRENCY: usize = 32;
pub struct ParallelCacheOps;
impl ParallelCacheOps {
pub async fn get_many_json<S: CacheStore>(
store: &S,
keys: &[&str],
) -> CacheResult<Vec<Option<String>>> {
store.mget(keys).await
}
pub async fn get_many<S: CacheStore, T: DeserializeOwned>(
store: &S,
keys: &[&str],
) -> CacheResult<Vec<Option<T>>> {
let json_values = Self::get_many_json(store, keys).await?;
json_values
.into_iter()
.map(|opt_json| {
opt_json
.map(|json| {
serde_json::from_str(&json)
.map_err(|e| CacheError::Deserialization(e.to_string()))
})
.transpose()
})
.collect()
}
pub async fn set_many_json<S: CacheStore>(
store: &S,
items: &[(&str, String)],
ttl: Option<Duration>,
) -> CacheResult<()> {
store.mset(items, ttl).await
}
pub async fn set_many<S: CacheStore, T: Serialize>(
store: &S,
items: &[(&str, T)],
ttl: Option<Duration>,
) -> CacheResult<()> {
let json_items: Result<Vec<_>, _> = items
.iter()
.map(|(key, value)| {
serde_json::to_string(value)
.map(|json| (*key, json))
.map_err(|e| CacheError::Serialization(e.to_string()))
})
.collect();
let json_items = json_items?;
let item_refs: Vec<_> = json_items.iter().map(|(k, v)| (*k, v.clone())).collect();
Self::set_many_json(store, &item_refs, ttl).await
}
pub async fn delete_many<S: CacheStore>(store: &S, keys: &[&str]) -> CacheResult<()> {
store.mdel(keys).await
}
pub async fn exists_many<S: CacheStore>(store: &S, keys: &[&str]) -> CacheResult<Vec<bool>> {
let futures = keys.iter().map(|key| store.exists(key));
let results: Vec<CacheResult<bool>> = join_all(futures).await;
results.into_iter().collect()
}
pub async fn ttl_many<S: CacheStore>(
store: &S,
keys: &[&str],
) -> CacheResult<Vec<Option<Duration>>> {
let futures = keys.iter().map(|key| store.ttl(key));
let results: Vec<CacheResult<Option<Duration>>> = join_all(futures).await;
results.into_iter().collect()
}
pub async fn warm_cache<S, T, F, Fut>(
store: &S,
keys: &[&str],
ttl: Option<Duration>,
factory: F,
) -> CacheResult<()>
where
S: CacheStore,
T: Serialize,
F: Fn(&str) -> Fut,
Fut: std::future::Future<Output = CacheResult<T>>,
{
Self::warm_cache_with_concurrency(store, keys, ttl, DEFAULT_WARM_CONCURRENCY, factory).await
}
pub async fn warm_cache_with_concurrency<S, T, F, Fut>(
store: &S,
keys: &[&str],
ttl: Option<Duration>,
max_concurrent: usize,
factory: F,
) -> CacheResult<()>
where
S: CacheStore,
T: Serialize,
F: Fn(&str) -> Fut,
Fut: std::future::Future<Output = CacheResult<T>>,
{
let limit = max_concurrent.max(1);
let factory = &factory;
let mut warmed = futures::stream::iter(keys.iter().map(|key| async move {
let value = factory(key).await?;
let json = serde_json::to_string(&value)
.map_err(|e| CacheError::Serialization(e.to_string()))?;
store.set_json(key, json, ttl).await?;
Ok::<(), CacheError>(())
}))
.buffer_unordered(limit);
while let Some(result) = warmed.next().await {
result?;
}
Ok(())
}
}
pub async fn get_many_json<S: CacheStore>(
store: &S,
keys: &[&str],
) -> CacheResult<Vec<Option<String>>> {
ParallelCacheOps::get_many_json(store, keys).await
}
pub async fn get_many<S: CacheStore, T: DeserializeOwned>(
store: &S,
keys: &[&str],
) -> CacheResult<Vec<Option<T>>> {
ParallelCacheOps::get_many(store, keys).await
}
pub async fn set_many_json<S: CacheStore>(
store: &S,
items: &[(&str, String)],
ttl: Option<Duration>,
) -> CacheResult<()> {
ParallelCacheOps::set_many_json(store, items, ttl).await
}
pub async fn set_many<S: CacheStore, T: Serialize>(
store: &S,
items: &[(&str, T)],
ttl: Option<Duration>,
) -> CacheResult<()> {
ParallelCacheOps::set_many(store, items, ttl).await
}
pub async fn delete_many<S: CacheStore>(store: &S, keys: &[&str]) -> CacheResult<()> {
ParallelCacheOps::delete_many(store, keys).await
}
pub async fn get_many_as_map<S: CacheStore, T: DeserializeOwned>(
store: &S,
keys: &[&str],
) -> CacheResult<HashMap<String, T>> {
let values = get_many(store, keys).await?;
let map: HashMap<String, T> = keys
.iter()
.zip(values)
.filter_map(|(key, opt_value)| opt_value.map(|value| (key.to_string(), value)))
.collect();
Ok(map)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tiered::InMemoryCache;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Default)]
struct ConcurrencyProbe {
current: AtomicUsize,
peak: AtomicUsize,
total: AtomicUsize,
}
impl ConcurrencyProbe {
fn enter(&self) {
let current = self.current.fetch_add(1, Ordering::SeqCst) + 1;
self.total.fetch_add(1, Ordering::SeqCst);
self.peak.fetch_max(current, Ordering::SeqCst);
}
fn leave(&self) {
self.current.fetch_sub(1, Ordering::SeqCst);
}
}
#[tokio::test]
async fn test_warm_cache_bounds_factory_concurrency() {
let cache = InMemoryCache::new();
let probe = Arc::new(ConcurrencyProbe::default());
let keys: Vec<String> = (0..64).map(|i| format!("k{i}")).collect();
let key_refs: Vec<&str> = keys.iter().map(|k| k.as_str()).collect();
const LIMIT: usize = 4;
ParallelCacheOps::warm_cache_with_concurrency(
&cache,
&key_refs,
None,
LIMIT,
|key: &str| {
let probe = probe.clone();
let key = key.to_string();
async move {
probe.enter();
for _ in 0..3 {
tokio::task::yield_now().await;
}
probe.leave();
Ok::<String, CacheError>(format!("value-for-{key}"))
}
},
)
.await
.unwrap();
assert_eq!(probe.total.load(Ordering::SeqCst), 64);
assert!(
probe.peak.load(Ordering::SeqCst) <= LIMIT,
"warm_cache ran {} factories at once, limit was {LIMIT}",
probe.peak.load(Ordering::SeqCst)
);
assert_eq!(
cache.get_json("k0").await.unwrap(),
Some("\"value-for-k0\"".to_string())
);
assert_eq!(
cache.get_json("k63").await.unwrap(),
Some("\"value-for-k63\"".to_string())
);
}
#[tokio::test]
async fn test_warm_cache_default_limit_applies() {
let cache = InMemoryCache::new();
let probe = Arc::new(ConcurrencyProbe::default());
let keys: Vec<String> = (0..DEFAULT_WARM_CONCURRENCY * 4)
.map(|i| format!("k{i}"))
.collect();
let key_refs: Vec<&str> = keys.iter().map(|k| k.as_str()).collect();
ParallelCacheOps::warm_cache(&cache, &key_refs, None, |_key: &str| {
let probe = probe.clone();
async move {
probe.enter();
for _ in 0..3 {
tokio::task::yield_now().await;
}
probe.leave();
Ok::<u32, CacheError>(1)
}
})
.await
.unwrap();
assert!(probe.peak.load(Ordering::SeqCst) <= DEFAULT_WARM_CONCURRENCY);
assert_eq!(
probe.total.load(Ordering::SeqCst),
DEFAULT_WARM_CONCURRENCY * 4
);
}
#[tokio::test]
async fn test_warm_cache_zero_concurrency_is_clamped_to_one() {
let cache = InMemoryCache::new();
let probe = Arc::new(ConcurrencyProbe::default());
ParallelCacheOps::warm_cache_with_concurrency(
&cache,
&["a", "b", "c"],
None,
0,
|_key: &str| {
let probe = probe.clone();
async move {
probe.enter();
tokio::task::yield_now().await;
probe.leave();
Ok::<u32, CacheError>(7)
}
},
)
.await
.unwrap();
assert_eq!(probe.peak.load(Ordering::SeqCst), 1);
assert_eq!(probe.total.load(Ordering::SeqCst), 3);
assert_eq!(cache.get_json("c").await.unwrap(), Some("7".to_string()));
}
#[tokio::test]
async fn test_warm_cache_propagates_factory_error() {
let cache = InMemoryCache::new();
let result = ParallelCacheOps::warm_cache_with_concurrency(
&cache,
&["a", "b"],
None,
1,
|key: &str| {
let fails = key == "b";
async move {
if fails {
Err(CacheError::Other("factory failed".to_string()))
} else {
Ok(1_u32)
}
}
},
)
.await;
assert!(result.is_err());
}
}