use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use ppoppo_infra::{Cache as InfraCache, CacheExt as _};
use super::Cache;
pub struct SharedCacheCache {
inner: Arc<dyn InfraCache>,
}
impl SharedCacheCache {
#[must_use]
pub fn new(inner: Arc<dyn InfraCache>) -> Self {
Self { inner }
}
}
impl std::fmt::Debug for SharedCacheCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedCacheCache").finish_non_exhaustive()
}
}
#[async_trait]
impl Cache for SharedCacheCache {
async fn get(&self, key: &str) -> Option<i64> {
self.inner.get_typed::<i64>(key).await.ok().flatten()
}
async fn set(&self, key: &str, sv: i64, ttl: Duration) {
let ttl_secs = ttl.as_secs().max(1).min(i32::MAX as u64) as i32;
let _ = self.inner.set_typed(key, &sv, Some(ttl_secs)).await;
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unimplemented
)]
mod tests {
use super::*;
use async_trait::async_trait;
use ppoppo_infra::Result as InfraResult;
use ppoppo_token::sv_cache_key;
use serde_json::Value as Json;
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Default)]
struct MemCache {
store: Mutex<HashMap<String, Json>>,
last_set_ttl: Mutex<Option<i32>>,
get_should_error: Mutex<bool>,
}
#[async_trait]
impl InfraCache for MemCache {
async fn get(&self, key: &str) -> InfraResult<Option<Json>> {
if *self.get_should_error.lock().unwrap() {
return Err(ppoppo_infra::Error::NotFound("simulated".into()));
}
Ok(self.store.lock().unwrap().get(key).cloned())
}
async fn set(
&self,
key: &str,
value: &Json,
ttl_seconds: Option<i32>,
) -> InfraResult<()> {
*self.last_set_ttl.lock().unwrap() = ttl_seconds;
self.store
.lock()
.unwrap()
.insert(key.to_string(), value.clone());
Ok(())
}
async fn del(&self, _key: &str) -> InfraResult<bool> {
unimplemented!("not exercised by SharedCacheCache")
}
async fn exists(&self, _key: &str) -> InfraResult<bool> {
unimplemented!("not exercised by SharedCacheCache")
}
async fn ttl(&self, _key: &str) -> InfraResult<Option<i32>> {
unimplemented!("not exercised by SharedCacheCache")
}
async fn mset(
&self,
_entries: &[(&str, Json, Option<i32>)],
) -> InfraResult<usize> {
unimplemented!("not exercised by SharedCacheCache")
}
async fn mget(&self, _keys: &[&str]) -> InfraResult<Vec<(String, Option<Json>)>> {
unimplemented!("not exercised by SharedCacheCache")
}
async fn mdel(&self, _keys: &[&str]) -> InfraResult<usize> {
unimplemented!("not exercised by SharedCacheCache")
}
async fn keys(&self, _pattern: &str, _limit: i32) -> InfraResult<Vec<String>> {
unimplemented!("not exercised by SharedCacheCache")
}
}
const SUB: &str = "01HSAB00000000000000000000";
#[tokio::test]
async fn set_writes_through_with_ttl_in_seconds() {
let mem = Arc::new(MemCache::default());
let cache = SharedCacheCache::new(mem.clone() as Arc<dyn InfraCache>);
let key = sv_cache_key(SUB);
cache.set(&key, 42, Duration::from_secs(60)).await;
let stored = mem.store.lock().unwrap().get(&key).cloned();
assert_eq!(stored, Some(serde_json::json!(42)));
assert_eq!(*mem.last_set_ttl.lock().unwrap(), Some(60));
}
#[tokio::test]
async fn set_clamps_subsecond_ttl_to_one() {
let mem = Arc::new(MemCache::default());
let cache = SharedCacheCache::new(mem.clone() as Arc<dyn InfraCache>);
cache
.set(&sv_cache_key(SUB), 7, Duration::from_millis(500))
.await;
assert_eq!(*mem.last_set_ttl.lock().unwrap(), Some(1));
}
#[tokio::test]
async fn get_returns_stored_i64() {
let mem = Arc::new(MemCache::default());
let key = sv_cache_key(SUB);
mem.store
.lock()
.unwrap()
.insert(key.clone(), serde_json::json!(13));
let cache = SharedCacheCache::new(mem.clone() as Arc<dyn InfraCache>);
assert_eq!(cache.get(&key).await, Some(13));
}
#[tokio::test]
async fn get_returns_none_on_miss() {
let mem = Arc::new(MemCache::default());
let cache = SharedCacheCache::new(mem as Arc<dyn InfraCache>);
assert_eq!(cache.get("sv:nonexistent").await, None);
}
#[tokio::test]
async fn get_swallows_substrate_errors_as_none() {
let mem = Arc::new(MemCache::default());
*mem.get_should_error.lock().unwrap() = true;
let cache = SharedCacheCache::new(mem as Arc<dyn InfraCache>);
assert_eq!(cache.get("sv:abc").await, None);
}
}