1use std::{
5 collections::HashMap,
6 sync::{
7 Arc, RwLock, Weak,
8 atomic::{AtomicU64, Ordering},
9 },
10};
11
12use object_store::path::Path;
13use url::Url;
14
15use crate::object_store::WrappingObjectStore;
16use crate::object_store::uri_to_url;
17
18use super::{ObjectStore, ObjectStoreParams, tracing::ObjectStoreTracingExt};
19use lance_core::error::{Error, LanceOptionExt, Result};
20
21#[cfg(feature = "aws")]
22pub mod aws;
23#[cfg(feature = "azure")]
24pub mod azure;
25#[cfg(feature = "gcp")]
26pub mod gcp;
27#[cfg(feature = "goosefs")]
28pub mod goosefs;
29#[cfg(feature = "huggingface")]
30pub mod huggingface;
31pub mod local;
32pub mod memory;
33#[cfg(feature = "oss")]
34pub mod oss;
35pub mod shared_memory;
36#[cfg(feature = "tencent")]
37pub mod tencent;
38#[cfg(feature = "tos")]
39pub mod tos;
40
41#[async_trait::async_trait]
42pub trait ObjectStoreProvider: std::fmt::Debug + Sync + Send {
43 async fn new_store(&self, base_path: Url, params: &ObjectStoreParams) -> Result<ObjectStore>;
44
45 fn extract_path(&self, url: &Url) -> Result<Path> {
53 Path::from_url_path(url.path()).map_err(|e| {
58 Error::invalid_input(format!("Invalid path in URL '{}': {}", url.path(), e))
59 })
60 }
61
62 fn calculate_object_store_prefix(
75 &self,
76 url: &Url,
77 _storage_options: Option<&HashMap<String, String>>,
78 ) -> Result<String> {
79 Ok(format!("{}${}", url.scheme(), url.authority()))
80 }
81}
82
83#[derive(Debug, Clone, Default)]
85pub struct ObjectStoreRegistryStats {
86 pub hits: u64,
88 pub misses: u64,
90 pub active_stores: usize,
92}
93
94#[derive(Debug)]
116pub struct ObjectStoreRegistry {
117 providers: RwLock<HashMap<String, Arc<dyn ObjectStoreProvider>>>,
118 active_stores: RwLock<HashMap<(String, ObjectStoreParams), Weak<ObjectStore>>>,
122 hits: AtomicU64,
124 misses: AtomicU64,
125}
126
127impl ObjectStoreRegistry {
128 pub fn empty() -> Self {
133 Self {
134 providers: RwLock::new(HashMap::new()),
135 active_stores: RwLock::new(HashMap::new()),
136 hits: AtomicU64::new(0),
137 misses: AtomicU64::new(0),
138 }
139 }
140
141 pub fn get_provider(&self, scheme: &str) -> Option<Arc<dyn ObjectStoreProvider>> {
143 self.providers
144 .read()
145 .expect("ObjectStoreRegistry lock poisoned")
146 .get(scheme)
147 .cloned()
148 }
149
150 pub fn active_stores(&self) -> Vec<Arc<ObjectStore>> {
155 let mut found_inactive = false;
156 let output = self
157 .active_stores
158 .read()
159 .expect("ObjectStoreRegistry lock poisoned")
160 .values()
161 .filter_map(|weak| match weak.upgrade() {
162 Some(store) => Some(store),
163 None => {
164 found_inactive = true;
165 None
166 }
167 })
168 .collect();
169
170 if found_inactive {
171 let mut cache_lock = self
173 .active_stores
174 .write()
175 .expect("ObjectStoreRegistry lock poisoned");
176 cache_lock.retain(|_, weak| weak.upgrade().is_some());
177 }
178 output
179 }
180
181 pub fn stats(&self) -> ObjectStoreRegistryStats {
187 let active_stores = self
188 .active_stores
189 .read()
190 .map(|s| s.values().filter(|w| w.strong_count() > 0).count())
191 .unwrap_or(0);
192 ObjectStoreRegistryStats {
193 hits: self.hits.load(Ordering::Relaxed),
194 misses: self.misses.load(Ordering::Relaxed),
195 active_stores,
196 }
197 }
198
199 fn scheme_not_found_error(&self, scheme: &str) -> Error {
200 let mut message = format!("No object store provider found for scheme: '{}'", scheme);
201 if let Ok(providers) = self.providers.read() {
202 let valid_schemes = providers.keys().cloned().collect::<Vec<_>>().join(", ");
203 message.push_str(&format!("\nValid schemes: {}", valid_schemes));
204 }
205 Error::invalid_input(message)
206 }
207
208 async fn build_store(
209 &self,
210 provider: Arc<dyn ObjectStoreProvider>,
211 base_path: Url,
212 params: &ObjectStoreParams,
213 metrics_base: &str,
214 ) -> Result<Arc<ObjectStore>> {
215 let mut store = provider.new_store(base_path, params).await?;
216 if let Some(block_size) = params.resolved_block_size()? {
219 store.block_size = block_size;
220 }
221
222 store.inner = store.inner.traced();
223
224 crate::object_store::meter_store(&mut store.inner, &mut store.io_tracker, metrics_base);
228
229 if let Some(wrapper) = ¶ms.object_store_wrapper {
230 store.apply_wrapper(wrapper.as_ref());
231 }
232
233 store.inner = store.io_tracker.wrap("", store.inner);
235
236 Ok(Arc::new(store))
237 }
238
239 #[doc(hidden)]
244 pub async fn new_store(
245 &self,
246 base_path: Url,
247 params: &ObjectStoreParams,
248 ) -> Result<Arc<ObjectStore>> {
249 let params = params.scoped_to_base(None);
253 let params = params.as_ref();
254 let scheme = base_path.scheme();
255 let Some(provider) = self.get_provider(scheme) else {
256 return Err(self.scheme_not_found_error(scheme));
257 };
258 let store_prefix =
259 provider.calculate_object_store_prefix(&base_path, params.storage_options())?;
260 let metrics_base = crate::object_store::metrics_base(&store_prefix, &base_path);
261
262 self.build_store(provider, base_path, params, &metrics_base)
263 .await
264 }
265
266 pub async fn get_store(
272 &self,
273 base_path: Url,
274 params: &ObjectStoreParams,
275 ) -> Result<Arc<ObjectStore>> {
276 let params = params.scoped_to_base(None);
281 let params = params.as_ref();
282 let scheme = base_path.scheme();
283 let Some(provider) = self.get_provider(scheme) else {
284 return Err(self.scheme_not_found_error(scheme));
285 };
286
287 let store_prefix =
291 provider.calculate_object_store_prefix(&base_path, params.storage_options())?;
292 let cache_path = crate::object_store::metrics_base(&store_prefix, &base_path);
293 let cache_key = (cache_path.clone(), params.clone());
294
295 {
297 let maybe_store = self
298 .active_stores
299 .read()
300 .ok()
301 .expect_ok()?
302 .get(&cache_key)
303 .cloned();
304 if let Some(store) = maybe_store {
305 if let Some(store) = store.upgrade() {
306 self.hits.fetch_add(1, Ordering::Relaxed);
307 return Ok(store);
308 } else {
309 let mut cache_lock = self
311 .active_stores
312 .write()
313 .expect("ObjectStoreRegistry lock poisoned");
314 if let Some(store) = cache_lock.get(&cache_key)
315 && store.upgrade().is_none()
316 {
317 cache_lock.remove(&cache_key);
319 }
320 }
321 }
322 }
323
324 self.misses.fetch_add(1, Ordering::Relaxed);
325
326 let store = self
327 .build_store(provider, base_path, params, &cache_path)
328 .await?;
329
330 {
331 let mut cache_lock = self.active_stores.write().ok().expect_ok()?;
333 cache_lock.insert(cache_key, Arc::downgrade(&store));
334 }
335
336 Ok(store)
337 }
338
339 pub fn calculate_object_store_prefix(
342 &self,
343 uri: &str,
344 storage_options: Option<&HashMap<String, String>>,
345 ) -> Result<String> {
346 let url = uri_to_url(uri)?;
347 match self.get_provider(url.scheme()) {
348 None => {
349 if url.scheme() == "file" || url.scheme().len() == 1 {
350 Ok("file".to_string())
351 } else {
352 Err(self.scheme_not_found_error(url.scheme()))
353 }
354 }
355 Some(provider) => provider.calculate_object_store_prefix(&url, storage_options),
356 }
357 }
358}
359
360impl Default for ObjectStoreRegistry {
361 fn default() -> Self {
362 let mut providers: HashMap<String, Arc<dyn ObjectStoreProvider>> = HashMap::new();
363
364 providers.insert("memory".into(), Arc::new(memory::MemoryStoreProvider));
365 providers.insert(
366 "shared-memory".into(),
367 Arc::new(shared_memory::SharedMemoryStoreProvider::default()),
368 );
369 providers.insert("file".into(), Arc::new(local::FileStoreProvider));
370 providers.insert(
376 "file-object-store".into(),
377 Arc::new(local::FileStoreProvider),
378 );
379 #[cfg(target_os = "linux")]
380 providers.insert("file+uring".into(), Arc::new(local::FileStoreProvider));
381
382 #[cfg(feature = "aws")]
383 {
384 let aws = Arc::new(aws::AwsStoreProvider);
385 providers.insert("s3".into(), aws.clone());
386 providers.insert("s3+ddb".into(), aws);
387 }
388 #[cfg(feature = "azure")]
389 {
390 let azure = Arc::new(azure::AzureBlobStoreProvider);
391 providers.insert("az".into(), azure.clone());
392 providers.insert("abfss".into(), azure);
393 }
394 #[cfg(feature = "gcp")]
395 providers.insert("gs".into(), Arc::new(gcp::GcsStoreProvider));
396 #[cfg(feature = "goosefs")]
397 providers.insert("goosefs".into(), Arc::new(goosefs::GooseFsStoreProvider));
398 #[cfg(feature = "oss")]
399 providers.insert("oss".into(), Arc::new(oss::OssStoreProvider));
400 #[cfg(feature = "tencent")]
401 providers.insert("cos".into(), Arc::new(tencent::TencentStoreProvider));
402 #[cfg(feature = "huggingface")]
403 providers.insert("hf".into(), Arc::new(huggingface::HuggingfaceStoreProvider));
404 #[cfg(feature = "tos")]
405 providers.insert("tos".into(), Arc::new(tos::TosStoreProvider));
406 Self {
407 providers: RwLock::new(providers),
408 active_stores: RwLock::new(HashMap::new()),
409 hits: AtomicU64::new(0),
410 misses: AtomicU64::new(0),
411 }
412 }
413}
414
415impl ObjectStoreRegistry {
416 pub fn insert(&self, scheme: &str, provider: Arc<dyn ObjectStoreProvider>) {
419 self.providers
420 .write()
421 .expect("ObjectStoreRegistry lock poisoned")
422 .insert(scheme.into(), provider);
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use std::collections::HashMap;
429 use std::sync::Mutex;
430
431 use super::*;
432 use object_store::ObjectStore as OSObjectStore;
433
434 use crate::object_store::providers::memory::MemoryStoreProvider;
435 use object_store::list::{PaginatedListOptions, PaginatedListResult, PaginatedListStore};
436 use rstest::rstest;
437
438 #[derive(Debug)]
439 struct DummyProvider;
440
441 #[async_trait::async_trait]
442 impl ObjectStoreProvider for DummyProvider {
443 async fn new_store(
444 &self,
445 _base_path: Url,
446 _params: &ObjectStoreParams,
447 ) -> Result<ObjectStore> {
448 unreachable!("This test doesn't create stores")
449 }
450 }
451
452 struct StubLister;
454
455 #[async_trait::async_trait]
456 impl PaginatedListStore for StubLister {
457 async fn list_paginated(
458 &self,
459 _prefix: Option<&str>,
460 _opts: PaginatedListOptions,
461 ) -> object_store::Result<PaginatedListResult> {
462 unimplemented!("this lister exists to be wrapped, not to list")
463 }
464 }
465
466 #[derive(Debug)]
468 struct PaginatedProvider;
469
470 #[async_trait::async_trait]
471 impl ObjectStoreProvider for PaginatedProvider {
472 async fn new_store(
473 &self,
474 base_path: Url,
475 params: &ObjectStoreParams,
476 ) -> Result<ObjectStore> {
477 let mut store = MemoryStoreProvider.new_store(base_path, params).await?;
478 store.paginated_lister = Some(Arc::new(StubLister));
479 Ok(store)
480 }
481
482 fn calculate_object_store_prefix(
483 &self,
484 _url: &Url,
485 _storage_options: Option<&HashMap<String, String>>,
486 ) -> Result<String> {
487 Ok("memory".to_string())
488 }
489 }
490
491 #[derive(Debug)]
495 struct RecordingWrapper {
496 keep_pushdown: bool,
497 prefixes: Mutex<Vec<String>>,
498 }
499
500 impl WrappingObjectStore for RecordingWrapper {
501 fn wrap(
502 &self,
503 store_prefix: &str,
504 _original: Arc<dyn OSObjectStore>,
505 ) -> Arc<dyn OSObjectStore> {
506 self.prefixes
507 .lock()
508 .unwrap()
509 .push(format!("wrap@{store_prefix}"));
510 Arc::new(object_store::memory::InMemory::new())
511 }
512
513 fn wrap_paginated(
514 &self,
515 store_prefix: &str,
516 original: Arc<dyn PaginatedListStore>,
517 ) -> Option<Arc<dyn PaginatedListStore>> {
518 self.prefixes
519 .lock()
520 .unwrap()
521 .push(format!("wrap_paginated@{store_prefix}"));
522 self.keep_pushdown.then_some(original)
523 }
524 }
525
526 #[rstest]
531 #[case::keeps_the_pushdown(true)]
532 #[case::gives_up_the_pushdown(false)]
533 #[tokio::test]
534 async fn test_the_registry_hands_the_lister_to_the_wrapper(#[case] keep_pushdown: bool) {
535 let wrapper = Arc::new(RecordingWrapper {
536 keep_pushdown,
537 prefixes: Mutex::new(Vec::new()),
538 });
539 let registry = ObjectStoreRegistry::default();
540 registry.insert("pagmem", Arc::new(PaginatedProvider));
541
542 let store = registry
543 .get_store(
544 Url::parse("pagmem:///").unwrap(),
545 &ObjectStoreParams {
546 object_store_wrapper: Some(wrapper.clone()),
547 ..Default::default()
548 },
549 )
550 .await
551 .unwrap();
552
553 assert_eq!(store.paginated_lister.is_some(), keep_pushdown);
554 assert_eq!(
556 *wrapper.prefixes.lock().unwrap(),
557 vec!["wrap@memory", "wrap_paginated@memory"]
558 );
559 if !keep_pushdown {
560 let page = store
563 .read_dir_page(Path::from(""), Default::default())
564 .await
565 .unwrap();
566 assert!(page.result.common_prefixes.is_empty());
567 assert!(page.result.objects.is_empty());
568 }
569 }
570
571 #[test]
572 fn test_calculate_object_store_prefix() {
573 let provider = DummyProvider;
574 let url = Url::parse("dummy://blah/path").unwrap();
575 assert_eq!(
576 "dummy$blah",
577 provider.calculate_object_store_prefix(&url, None).unwrap()
578 );
579 }
580
581 #[tokio::test]
582 async fn test_get_store_resolves_base_scoped_options() {
583 use crate::object_store::StorageOptionsAccessor;
584
585 let registry = ObjectStoreRegistry::default();
586 let url = Url::parse("memory://test").unwrap();
587
588 let with_scoped = ObjectStoreParams {
589 storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options(
590 HashMap::from([
591 ("shared".to_string(), "value".to_string()),
592 ("base_1.account_key".to_string(), "base1-key".to_string()),
593 ]),
594 ))),
595 ..Default::default()
596 };
597 let without_scoped = ObjectStoreParams {
598 storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options(
599 HashMap::from([("shared".to_string(), "value".to_string())]),
600 ))),
601 ..Default::default()
602 };
603
604 let store_scoped = registry.get_store(url.clone(), &with_scoped).await.unwrap();
607 let store_plain = registry.get_store(url, &without_scoped).await.unwrap();
608 assert!(Arc::ptr_eq(&store_scoped, &store_plain));
609 }
610
611 #[test]
612 fn test_calculate_object_store_scheme_not_found() {
613 let registry = ObjectStoreRegistry::empty();
614 registry.insert("dummy", Arc::new(DummyProvider));
615 let s = "Invalid user input: No object store provider found for scheme: 'dummy2'\nValid schemes: dummy";
616 let result = registry
617 .calculate_object_store_prefix("dummy2://mybucket/my/long/path", None)
618 .expect_err("expected error")
619 .to_string();
620 assert_eq!(s, &result[..s.len()]);
621 }
622
623 #[test]
625 fn test_calculate_object_store_prefix_for_local() {
626 let registry = ObjectStoreRegistry::empty();
627 assert_eq!(
628 "file",
629 registry
630 .calculate_object_store_prefix("/tmp/foobar", None)
631 .unwrap()
632 );
633 }
634
635 #[test]
637 fn test_calculate_object_store_prefix_for_local_windows_path() {
638 let registry = ObjectStoreRegistry::empty();
639 assert_eq!(
640 "file",
641 registry
642 .calculate_object_store_prefix("c://dos/path", None)
643 .unwrap()
644 );
645 }
646
647 #[test]
649 fn test_calculate_object_store_prefix_for_dummy_path() {
650 let registry = ObjectStoreRegistry::empty();
651 registry.insert("dummy", Arc::new(DummyProvider));
652 assert_eq!(
653 "dummy$mybucket",
654 registry
655 .calculate_object_store_prefix("dummy://mybucket/my/long/path", None)
656 .unwrap()
657 );
658 }
659
660 #[tokio::test]
661 async fn test_stats_hit_miss_tracking() {
662 use crate::object_store::StorageOptionsAccessor;
663 let registry = ObjectStoreRegistry::default();
664 let url = Url::parse("memory://test").unwrap();
665
666 let params1 = ObjectStoreParams::default();
667 let params2 = ObjectStoreParams {
668 storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options(
669 HashMap::from([("k".into(), "v".into())]),
670 ))),
671 ..Default::default()
672 };
673
674 let cases: &[(&ObjectStoreParams, (u64, u64, usize))] = &[
676 (¶ms1, (0, 1, 1)), (¶ms1, (1, 1, 1)), (¶ms2, (1, 2, 2)), ];
680
681 let mut stores = vec![]; for (params, (hits, misses, active)) in cases {
683 stores.push(registry.get_store(url.clone(), params).await.unwrap());
684 let s = registry.stats();
685 assert_eq!(
686 (s.hits, s.misses, s.active_stores),
687 (*hits, *misses, *active)
688 );
689 }
690
691 assert!(Arc::ptr_eq(&stores[0], &stores[1]));
693 }
694
695 #[tokio::test]
696 async fn test_new_store_bypasses_cache() {
697 let registry = ObjectStoreRegistry::default();
698 let url = Url::parse("memory://test").unwrap();
699 let params = ObjectStoreParams::default();
700
701 let first = registry.new_store(url.clone(), ¶ms).await.unwrap();
702 let second = registry.new_store(url, ¶ms).await.unwrap();
703
704 assert!(!Arc::ptr_eq(&first, &second));
705 let stats = registry.stats();
706 assert_eq!((stats.hits, stats.misses, stats.active_stores), (0, 0, 0));
707 }
708}