facto_core/registries/
cache.rs1use std::future::Future;
10use std::hash::Hash;
11use std::pin::Pin;
12use std::sync::Arc;
13use std::time::Duration;
14
15use dashmap::DashMap;
16use tokio::time::Instant;
17
18use crate::config::CacheTtlConfig;
19use crate::models::*;
20use crate::registries::{Registry, RegistryResult};
21
22type CacheMap<K, V> = Arc<DashMap<K, (V, Instant)>>;
23
24pub struct CachedRegistry<R: Registry> {
25 inner: R,
26 get_package_cache: CacheMap<String, PackageInfo>,
27 get_versions_cache: CacheMap<String, Vec<VersionInfo>>,
28 search_cache: CacheMap<(String, usize), Vec<SearchResult>>,
29 ttl: CacheTtlConfig,
30}
31
32impl<R: Registry> CachedRegistry<R> {
33 pub fn new(inner: R, ttl: CacheTtlConfig) -> Self {
34 Self {
35 inner,
36 get_package_cache: Arc::new(DashMap::new()),
37 get_versions_cache: Arc::new(DashMap::new()),
38 search_cache: Arc::new(DashMap::new()),
39 ttl,
40 }
41 }
42}
43
44fn get_fresh<K, V>(cache: &DashMap<K, (V, Instant)>, key: &K, ttl: Duration) -> Option<V>
45where
46 K: Hash + Eq,
47 V: Clone,
48{
49 cache
50 .get(key)
51 .filter(|e| e.1.elapsed() < ttl)
52 .map(|e| e.0.clone())
53}
54
55impl<R: Registry + 'static> Registry for CachedRegistry<R> {
56 fn id(&self) -> &str {
57 self.inner.id()
58 }
59
60 fn display_name(&self) -> &str {
61 self.inner.display_name()
62 }
63
64 fn supports_latest_version(&self) -> bool {
65 self.inner.supports_latest_version()
66 }
67
68 fn get_package<'a>(
69 &'a self,
70 name: &'a str,
71 ) -> Pin<Box<dyn Future<Output = RegistryResult<PackageInfo>> + Send + 'a>> {
72 Box::pin(async move {
73 let ttl = Duration::from_secs(self.ttl.package_secs);
74 let key = name.to_string();
75 if let Some(v) = get_fresh(&self.get_package_cache, &key, ttl) {
76 return Ok(v);
77 }
78 let v = self.inner.get_package(name).await?;
79 self.get_package_cache
80 .insert(key, (v.clone(), Instant::now()));
81 Ok(v)
82 })
83 }
84
85 fn get_versions<'a>(
86 &'a self,
87 name: &'a str,
88 ) -> Pin<Box<dyn Future<Output = RegistryResult<Vec<VersionInfo>>> + Send + 'a>> {
89 Box::pin(async move {
90 let ttl = Duration::from_secs(self.ttl.package_secs);
91 let key = name.to_string();
92 if let Some(v) = get_fresh(&self.get_versions_cache, &key, ttl) {
93 return Ok(v);
94 }
95 let v = self.inner.get_versions(name).await?;
96 self.get_versions_cache
97 .insert(key, (v.clone(), Instant::now()));
98 Ok(v)
99 })
100 }
101
102 fn search<'a>(
103 &'a self,
104 query: &'a str,
105 limit: usize,
106 ) -> Pin<Box<dyn Future<Output = RegistryResult<Vec<SearchResult>>> + Send + 'a>> {
107 Box::pin(async move {
108 let ttl = Duration::from_secs(self.ttl.search_secs);
109 let key = (query.to_string(), limit);
110 if let Some(v) = get_fresh(&self.search_cache, &key, ttl) {
111 return Ok(v);
112 }
113 let v = self.inner.search(query, limit).await?;
114 self.search_cache.insert(key, (v.clone(), Instant::now()));
115 Ok(v)
116 })
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::registries::{Registry, RegistryError, RegistryResult};
124 use std::sync::atomic::{AtomicUsize, Ordering};
125
126 struct CountingRegistry {
127 get_package_calls: AtomicUsize,
128 }
129
130 impl Registry for CountingRegistry {
131 fn id(&self) -> &str {
132 "counting"
133 }
134 fn display_name(&self) -> &str {
135 "Counting"
136 }
137
138 fn supports_latest_version(&self) -> bool {
139 false
140 }
141
142 fn get_package<'a>(
143 &'a self,
144 _name: &'a str,
145 ) -> Pin<Box<dyn Future<Output = RegistryResult<PackageInfo>> + Send + 'a>> {
146 Box::pin(async move {
147 self.get_package_calls.fetch_add(1, Ordering::SeqCst);
148 Ok(PackageInfo {
149 name: "foo".into(),
150 registry: "counting".into(),
151 latest_version: None,
152 description: None,
153 license: None,
154 homepage: None,
155 repository: None,
156 authors: Vec::new(),
157 updated_at: None,
158 keywords: Vec::new(),
159 classifiers: Vec::new(),
160 requires_python: None,
161 })
162 })
163 }
164
165 fn get_versions<'a>(
166 &'a self,
167 _name: &'a str,
168 ) -> Pin<Box<dyn Future<Output = RegistryResult<Vec<VersionInfo>>> + Send + 'a>> {
169 Box::pin(async move { Err(RegistryError::Parse("unused".into())) })
170 }
171
172 fn search<'a>(
173 &'a self,
174 _query: &'a str,
175 _limit: usize,
176 ) -> Pin<Box<dyn Future<Output = RegistryResult<Vec<SearchResult>>> + Send + 'a>> {
177 Box::pin(async move { Ok(Vec::new()) })
178 }
179 }
180
181 #[tokio::test]
182 async fn cached_registry_hits_cache_on_second_call() {
183 let inner = CountingRegistry {
184 get_package_calls: AtomicUsize::new(0),
185 };
186 let cached = CachedRegistry::new(inner, CacheTtlConfig::default());
187
188 cached.get_package("foo").await.unwrap();
189 cached.get_package("foo").await.unwrap();
190
191 assert_eq!(
192 cached.inner.get_package_calls.load(Ordering::SeqCst),
193 1,
194 "second call should have come from cache"
195 );
196 }
197
198 #[tokio::test]
199 async fn cached_registry_different_keys_hit_inner() {
200 let inner = CountingRegistry {
201 get_package_calls: AtomicUsize::new(0),
202 };
203 let cached = CachedRegistry::new(inner, CacheTtlConfig::default());
204
205 cached.get_package("foo").await.unwrap();
206 cached.get_package("bar").await.unwrap();
207
208 assert_eq!(cached.inner.get_package_calls.load(Ordering::SeqCst), 2);
209 }
210
211 #[tokio::test]
212 async fn cached_registry_expired_entries_refetch() {
213 let inner = CountingRegistry {
214 get_package_calls: AtomicUsize::new(0),
215 };
216 let ttl = CacheTtlConfig {
217 package_secs: 0, ..Default::default()
219 };
220 let cached = CachedRegistry::new(inner, ttl);
221
222 cached.get_package("foo").await.unwrap();
223 tokio::time::sleep(Duration::from_millis(10)).await;
224 cached.get_package("foo").await.unwrap();
225
226 assert_eq!(
227 cached.inner.get_package_calls.load(Ordering::SeqCst),
228 2,
229 "expired entry should refetch"
230 );
231 }
232
233 #[test]
234 fn forwards_supports_latest_version() {
235 let inner = CountingRegistry {
236 get_package_calls: AtomicUsize::new(0),
237 };
238 let cached = CachedRegistry::new(inner, CacheTtlConfig::default());
239 assert!(!cached.supports_latest_version());
240 }
241}