lance_core/cache/
registry.rs1use std::collections::HashMap;
18use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
19
20use super::backend::CacheBackend;
21use super::moka::{MOKA_BACKEND_KIND, build_moka};
22use crate::{Error, Result};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct BackendConfig {
30 pub kind: String,
32 pub options: HashMap<String, String>,
34}
35
36impl BackendConfig {
37 pub fn new(kind: impl AsRef<str>) -> Result<Self> {
39 Ok(Self {
40 kind: normalize_backend_kind(kind.as_ref())?,
41 options: HashMap::new(),
42 })
43 }
44
45 pub fn with_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
47 self.options.insert(key.into(), value.into());
48 self
49 }
50}
51
52pub fn normalize_backend_kind(kind: &str) -> Result<String> {
58 let mut chars = kind.chars();
59 match chars.next() {
60 Some(c) if c.is_ascii_alphabetic() => {}
61 _ => {
62 return Err(Error::invalid_input(format!(
63 "cache backend kind {:?}: must start with an ASCII letter",
64 kind
65 )));
66 }
67 }
68 for c in chars {
69 let ok = c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.');
70 if !ok {
71 return Err(Error::invalid_input(format!(
72 "cache backend kind {:?}: invalid character {:?}",
73 kind, c
74 )));
75 }
76 }
77 Ok(kind.to_ascii_lowercase())
78}
79
80pub type BackendBuildFn = fn(&BackendConfig) -> Result<Arc<dyn CacheBackend>>;
85
86fn registry() -> &'static Mutex<HashMap<String, BackendBuildFn>> {
87 static REGISTRY: OnceLock<Mutex<HashMap<String, BackendBuildFn>>> = OnceLock::new();
88 REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
89}
90
91fn registry_lock() -> Result<MutexGuard<'static, HashMap<String, BackendBuildFn>>> {
92 registry()
93 .lock()
94 .map_err(|_| Error::internal("cache backend registry mutex is poisoned"))
95}
96
97#[cfg(test)]
98fn registry_lock_for_test() -> MutexGuard<'static, HashMap<String, BackendBuildFn>> {
99 registry()
100 .lock()
101 .unwrap_or_else(|poisoned| poisoned.into_inner())
102}
103
104pub fn register_backend(kind: &str, build: BackendBuildFn) -> Result<()> {
127 let kind = normalize_backend_kind(kind)?;
128 insert_backend(&kind, build, builtin_backend(&kind).is_some())
129}
130
131fn insert_backend(kind: &str, build: BackendBuildFn, allow_replace: bool) -> Result<()> {
132 let mut map = registry_lock()?;
133 if map.contains_key(kind) && !allow_replace {
134 return Err(Error::invalid_input(format!(
135 "cache backend {:?} is already registered",
136 kind
137 )));
138 }
139 map.insert(kind.to_string(), build);
140 Ok(())
141}
142
143fn builtin_backend(kind: &str) -> Option<BackendBuildFn> {
144 match kind {
145 MOKA_BACKEND_KIND => Some(build_moka),
146 _ => None,
147 }
148}
149
150pub fn build_from_config(config: &BackendConfig) -> Result<Arc<dyn CacheBackend>> {
154 ensure_builtin_backends()?;
155 let kind = normalize_backend_kind(&config.kind)?;
156 let config = BackendConfig {
157 kind: kind.clone(),
158 options: config.options.clone(),
159 };
160 let build = {
161 let map = registry_lock()?;
162 map.get(&kind).copied()
163 };
164 match build {
165 Some(build) => build(&config),
166 None => Err(Error::invalid_input(format!(
167 "unknown cache backend kind: {:?}",
168 kind
169 ))),
170 }
171}
172
173fn ensure_builtin_backends() -> Result<()> {
186 let mut map = registry_lock()?;
187 if !map.contains_key(MOKA_BACKEND_KIND)
188 && let Some(build) = builtin_backend(MOKA_BACKEND_KIND)
189 {
190 map.insert(MOKA_BACKEND_KIND.to_string(), build);
191 }
192 Ok(())
193}
194
195#[cfg(test)]
198pub(super) fn take_registry_for_test() -> HashMap<String, BackendBuildFn> {
199 let mut map = registry_lock_for_test();
200 std::mem::take(&mut *map)
201}
202
203#[cfg(test)]
205pub(super) fn restore_registry_for_test(saved: HashMap<String, BackendBuildFn>) {
206 let mut map = registry_lock_for_test();
207 *map = saved;
208}
209
210#[cfg(test)]
211pub(super) fn registry_test_lock() -> std::sync::MutexGuard<'static, ()> {
212 static M: OnceLock<std::sync::Mutex<()>> = OnceLock::new();
213 M.get_or_init(|| std::sync::Mutex::new(()))
214 .lock()
215 .unwrap_or_else(|poisoned| poisoned.into_inner())
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use async_trait::async_trait;
222 use std::pin::Pin;
223
224 use crate::cache::InternalCacheKey;
225 use crate::cache::backend::CacheEntry;
226 use crate::cache::codec::CacheCodec;
227 use futures::Future;
228
229 #[derive(Debug, Default)]
232 struct NullBackend;
233
234 #[async_trait]
235 impl CacheBackend for NullBackend {
236 async fn get(
237 &self,
238 _key: &InternalCacheKey,
239 _codec: Option<CacheCodec>,
240 ) -> Option<CacheEntry> {
241 None
242 }
243
244 async fn insert(
245 &self,
246 _key: &InternalCacheKey,
247 _entry: CacheEntry,
248 _size_bytes: usize,
249 _codec: Option<CacheCodec>,
250 ) {
251 }
252
253 async fn get_or_insert<'a>(
254 &self,
255 _key: &InternalCacheKey,
256 loader: Pin<Box<dyn Future<Output = crate::Result<(CacheEntry, usize)>> + Send + 'a>>,
257 _codec: Option<CacheCodec>,
258 ) -> crate::Result<(CacheEntry, bool)> {
259 let (entry, _size) = loader.await?;
260 Ok((entry, false))
261 }
262
263 async fn clear(&self) {}
264 async fn num_entries(&self) -> usize {
265 0
266 }
267 async fn size_bytes(&self) -> usize {
268 0
269 }
270 }
271
272 fn build_null(_cfg: &BackendConfig) -> Result<Arc<dyn CacheBackend>> {
273 Ok(Arc::new(NullBackend))
274 }
275
276 struct RegistryGuard {
277 _lock: std::sync::MutexGuard<'static, ()>,
279 saved: HashMap<String, BackendBuildFn>,
280 }
281 impl RegistryGuard {
282 fn new() -> Self {
283 Self {
284 _lock: registry_test_lock(),
285 saved: take_registry_for_test(),
286 }
287 }
288 }
289 impl Drop for RegistryGuard {
290 fn drop(&mut self) {
291 restore_registry_for_test(std::mem::take(&mut self.saved));
292 }
293 }
294
295 #[test]
296 fn test_register_and_build() {
297 let _guard = RegistryGuard::new();
298 register_backend("null", build_null).unwrap();
299 let backend = build_from_config(&BackendConfig::new("null").unwrap()).unwrap();
300 assert_eq!(Arc::strong_count(&backend), 1);
303 }
304
305 #[test]
306 fn test_duplicate_registration_errors() {
307 let _guard = RegistryGuard::new();
308 register_backend("dup", build_null).unwrap();
309 let err = register_backend("dup", build_null).unwrap_err();
310 assert!(err.to_string().contains("already registered"));
311 }
312
313 #[test]
314 fn test_builtin_kind_can_be_overridden() {
315 let _guard = RegistryGuard::new();
316 register_backend("moka", build_null).unwrap();
317 let backend = build_from_config(&BackendConfig::new("moka").unwrap()).unwrap();
318 assert_eq!(Arc::strong_count(&backend), 1);
319 }
320
321 #[test]
322 fn test_unknown_kind_errors() {
323 let _guard = RegistryGuard::new();
324 let err = build_from_config(&BackendConfig::new("missing").unwrap()).unwrap_err();
325 assert!(err.to_string().contains("unknown cache backend kind"));
326 }
327
328 #[test]
329 fn test_backend_kind_is_normalized() {
330 let _guard = RegistryGuard::new();
331 register_backend("Echo.Backend", build_null).unwrap();
332 let backend = build_from_config(&BackendConfig::new("echo.backend").unwrap()).unwrap();
333 assert_eq!(Arc::strong_count(&backend), 1);
334 }
335
336 #[test]
337 fn test_config_lookup_normalizes_direct_config() {
338 let _guard = RegistryGuard::new();
339 fn build_echo(cfg: &BackendConfig) -> Result<Arc<dyn CacheBackend>> {
340 assert_eq!(cfg.kind, "echo.backend");
341 Ok(Arc::new(NullBackend))
342 }
343 register_backend("echo.backend", build_echo).unwrap();
344 let cfg = BackendConfig {
345 kind: "ECHO.Backend".to_string(),
346 options: HashMap::new(),
347 };
348 build_from_config(&cfg).unwrap();
349 }
350
351 #[test]
352 fn test_invalid_backend_kind_errors() {
353 let err = register_backend("not a scheme", build_null).unwrap_err();
354 assert!(err.to_string().contains("invalid character"));
355 let err = BackendConfig::new("1moka").unwrap_err();
356 assert!(err.to_string().contains("must start with an ASCII letter"));
357 }
358
359 #[test]
360 fn test_options_are_passed_through() {
361 let _guard = RegistryGuard::new();
362 fn build_echo(cfg: &BackendConfig) -> Result<Arc<dyn CacheBackend>> {
363 assert_eq!(cfg.options.get("capacity").map(String::as_str), Some("42"));
364 Ok(Arc::new(NullBackend))
365 }
366 register_backend("echo", build_echo).unwrap();
367 let cfg = BackendConfig::new("echo")
368 .unwrap()
369 .with_option("capacity", "42");
370 build_from_config(&cfg).unwrap();
371 }
372}