pas_external/epoch/
shared_cache.rs1use std::sync::Arc;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use ppoppo_infra::{Cache as InfraCache, CacheExt as _};
8
9use super::Cache;
10
11pub struct SharedCacheCache {
51 inner: Arc<dyn InfraCache>,
52}
53
54impl SharedCacheCache {
55 #[must_use]
69 pub fn new(inner: Arc<dyn InfraCache>) -> Self {
70 Self { inner }
71 }
72}
73
74impl std::fmt::Debug for SharedCacheCache {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct("SharedCacheCache").finish_non_exhaustive()
77 }
78}
79
80#[async_trait]
81impl Cache for SharedCacheCache {
82 async fn get(&self, key: &str) -> Option<i64> {
83 self.inner.get_typed::<i64>(key).await.ok().flatten()
84 }
85
86 async fn set(&self, key: &str, sv: i64, ttl: Duration) {
87 let ttl_secs = ttl.as_secs().max(1).min(i32::MAX as u64) as i32;
88 let _ = self.inner.set_typed(key, &sv, Some(ttl_secs)).await;
89 }
90}
91
92#[cfg(test)]
93#[allow(
94 clippy::unwrap_used,
95 clippy::expect_used,
96 clippy::panic,
97 clippy::unimplemented
98)]
99mod tests {
100 use super::*;
105 use async_trait::async_trait;
106 use ppoppo_infra::Result as InfraResult;
107 use ppoppo_token::sv_cache_key;
108 use serde_json::Value as Json;
109 use std::collections::HashMap;
110 use std::sync::Mutex;
111
112 #[derive(Default)]
113 struct MemCache {
114 store: Mutex<HashMap<String, Json>>,
115 last_set_ttl: Mutex<Option<i32>>,
116 get_should_error: Mutex<bool>,
117 }
118
119 #[async_trait]
120 impl InfraCache for MemCache {
121 async fn get(&self, key: &str) -> InfraResult<Option<Json>> {
122 if *self.get_should_error.lock().unwrap() {
123 return Err(ppoppo_infra::Error::NotFound("simulated".into()));
124 }
125 Ok(self.store.lock().unwrap().get(key).cloned())
126 }
127
128 async fn set(
129 &self,
130 key: &str,
131 value: &Json,
132 ttl_seconds: Option<i32>,
133 ) -> InfraResult<()> {
134 *self.last_set_ttl.lock().unwrap() = ttl_seconds;
135 self.store
136 .lock()
137 .unwrap()
138 .insert(key.to_string(), value.clone());
139 Ok(())
140 }
141
142 async fn del(&self, _key: &str) -> InfraResult<bool> {
143 unimplemented!("not exercised by SharedCacheCache")
144 }
145 async fn exists(&self, _key: &str) -> InfraResult<bool> {
146 unimplemented!("not exercised by SharedCacheCache")
147 }
148 async fn ttl(&self, _key: &str) -> InfraResult<Option<i32>> {
149 unimplemented!("not exercised by SharedCacheCache")
150 }
151 async fn mset(
152 &self,
153 _entries: &[(&str, Json, Option<i32>)],
154 ) -> InfraResult<usize> {
155 unimplemented!("not exercised by SharedCacheCache")
156 }
157 async fn mget(&self, _keys: &[&str]) -> InfraResult<Vec<(String, Option<Json>)>> {
158 unimplemented!("not exercised by SharedCacheCache")
159 }
160 async fn mdel(&self, _keys: &[&str]) -> InfraResult<usize> {
161 unimplemented!("not exercised by SharedCacheCache")
162 }
163 async fn keys(&self, _pattern: &str, _limit: i32) -> InfraResult<Vec<String>> {
164 unimplemented!("not exercised by SharedCacheCache")
165 }
166 }
167
168 const SUB: &str = "01HSAB00000000000000000000";
169
170 #[tokio::test]
171 async fn set_writes_through_with_ttl_in_seconds() {
172 let mem = Arc::new(MemCache::default());
173 let cache = SharedCacheCache::new(mem.clone() as Arc<dyn InfraCache>);
174 let key = sv_cache_key(SUB);
175
176 cache.set(&key, 42, Duration::from_secs(60)).await;
177
178 let stored = mem.store.lock().unwrap().get(&key).cloned();
179 assert_eq!(stored, Some(serde_json::json!(42)));
180 assert_eq!(*mem.last_set_ttl.lock().unwrap(), Some(60));
181 }
182
183 #[tokio::test]
184 async fn set_clamps_subsecond_ttl_to_one() {
185 let mem = Arc::new(MemCache::default());
189 let cache = SharedCacheCache::new(mem.clone() as Arc<dyn InfraCache>);
190
191 cache
192 .set(&sv_cache_key(SUB), 7, Duration::from_millis(500))
193 .await;
194
195 assert_eq!(*mem.last_set_ttl.lock().unwrap(), Some(1));
196 }
197
198 #[tokio::test]
199 async fn get_returns_stored_i64() {
200 let mem = Arc::new(MemCache::default());
201 let key = sv_cache_key(SUB);
202 mem.store
203 .lock()
204 .unwrap()
205 .insert(key.clone(), serde_json::json!(13));
206
207 let cache = SharedCacheCache::new(mem.clone() as Arc<dyn InfraCache>);
208 assert_eq!(cache.get(&key).await, Some(13));
209 }
210
211 #[tokio::test]
212 async fn get_returns_none_on_miss() {
213 let mem = Arc::new(MemCache::default());
214 let cache = SharedCacheCache::new(mem as Arc<dyn InfraCache>);
215 assert_eq!(cache.get("sv:nonexistent").await, None);
216 }
217
218 #[tokio::test]
219 async fn get_swallows_substrate_errors_as_none() {
220 let mem = Arc::new(MemCache::default());
224 *mem.get_should_error.lock().unwrap() = true;
225 let cache = SharedCacheCache::new(mem as Arc<dyn InfraCache>);
226 assert_eq!(cache.get("sv:abc").await, None);
227 }
228}