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 pub async fn get_store(
214 &self,
215 base_path: Url,
216 params: &ObjectStoreParams,
217 ) -> Result<Arc<ObjectStore>> {
218 let params = params.scoped_to_base(None);
223 let params = params.as_ref();
224 let scheme = base_path.scheme();
225 let Some(provider) = self.get_provider(scheme) else {
226 return Err(self.scheme_not_found_error(scheme));
227 };
228
229 let cache_path =
230 provider.calculate_object_store_prefix(&base_path, params.storage_options())?;
231 let cache_key = (cache_path.clone(), params.clone());
232
233 {
235 let maybe_store = self
236 .active_stores
237 .read()
238 .ok()
239 .expect_ok()?
240 .get(&cache_key)
241 .cloned();
242 if let Some(store) = maybe_store {
243 if let Some(store) = store.upgrade() {
244 self.hits.fetch_add(1, Ordering::Relaxed);
245 return Ok(store);
246 } else {
247 let mut cache_lock = self
249 .active_stores
250 .write()
251 .expect("ObjectStoreRegistry lock poisoned");
252 if let Some(store) = cache_lock.get(&cache_key)
253 && store.upgrade().is_none()
254 {
255 cache_lock.remove(&cache_key);
257 }
258 }
259 }
260 }
261
262 self.misses.fetch_add(1, Ordering::Relaxed);
263
264 let mut store = provider.new_store(base_path, params).await?;
265
266 store.inner = store.inner.traced();
267
268 #[cfg(feature = "metrics")]
269 {
270 use crate::object_store::metrics::ObjectStoreMetricsExt;
273 store.inner = store.inner.metered(cache_path.clone());
274 }
275
276 if let Some(wrapper) = ¶ms.object_store_wrapper {
277 store.inner = wrapper.wrap(&cache_path, store.inner);
278 }
279
280 store.inner = store.io_tracker.wrap("", store.inner);
282
283 let store = Arc::new(store);
284
285 {
286 let mut cache_lock = self.active_stores.write().ok().expect_ok()?;
288 cache_lock.insert(cache_key, Arc::downgrade(&store));
289 }
290
291 Ok(store)
292 }
293
294 pub fn calculate_object_store_prefix(
297 &self,
298 uri: &str,
299 storage_options: Option<&HashMap<String, String>>,
300 ) -> Result<String> {
301 let url = uri_to_url(uri)?;
302 match self.get_provider(url.scheme()) {
303 None => {
304 if url.scheme() == "file" || url.scheme().len() == 1 {
305 Ok("file".to_string())
306 } else {
307 Err(self.scheme_not_found_error(url.scheme()))
308 }
309 }
310 Some(provider) => provider.calculate_object_store_prefix(&url, storage_options),
311 }
312 }
313}
314
315impl Default for ObjectStoreRegistry {
316 fn default() -> Self {
317 let mut providers: HashMap<String, Arc<dyn ObjectStoreProvider>> = HashMap::new();
318
319 providers.insert("memory".into(), Arc::new(memory::MemoryStoreProvider));
320 providers.insert(
321 "shared-memory".into(),
322 Arc::new(shared_memory::SharedMemoryStoreProvider::default()),
323 );
324 providers.insert("file".into(), Arc::new(local::FileStoreProvider));
325 providers.insert(
331 "file-object-store".into(),
332 Arc::new(local::FileStoreProvider),
333 );
334 #[cfg(target_os = "linux")]
335 providers.insert("file+uring".into(), Arc::new(local::FileStoreProvider));
336
337 #[cfg(feature = "aws")]
338 {
339 let aws = Arc::new(aws::AwsStoreProvider);
340 providers.insert("s3".into(), aws.clone());
341 providers.insert("s3+ddb".into(), aws);
342 }
343 #[cfg(feature = "azure")]
344 {
345 let azure = Arc::new(azure::AzureBlobStoreProvider);
346 providers.insert("az".into(), azure.clone());
347 providers.insert("abfss".into(), azure);
348 }
349 #[cfg(feature = "gcp")]
350 providers.insert("gs".into(), Arc::new(gcp::GcsStoreProvider));
351 #[cfg(feature = "goosefs")]
352 providers.insert("goosefs".into(), Arc::new(goosefs::GooseFsStoreProvider));
353 #[cfg(feature = "oss")]
354 providers.insert("oss".into(), Arc::new(oss::OssStoreProvider));
355 #[cfg(feature = "tencent")]
356 providers.insert("cos".into(), Arc::new(tencent::TencentStoreProvider));
357 #[cfg(feature = "huggingface")]
358 providers.insert("hf".into(), Arc::new(huggingface::HuggingfaceStoreProvider));
359 #[cfg(feature = "tos")]
360 providers.insert("tos".into(), Arc::new(tos::TosStoreProvider));
361 Self {
362 providers: RwLock::new(providers),
363 active_stores: RwLock::new(HashMap::new()),
364 hits: AtomicU64::new(0),
365 misses: AtomicU64::new(0),
366 }
367 }
368}
369
370impl ObjectStoreRegistry {
371 pub fn insert(&self, scheme: &str, provider: Arc<dyn ObjectStoreProvider>) {
374 self.providers
375 .write()
376 .expect("ObjectStoreRegistry lock poisoned")
377 .insert(scheme.into(), provider);
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use std::collections::HashMap;
384
385 use super::*;
386
387 #[derive(Debug)]
388 struct DummyProvider;
389
390 #[async_trait::async_trait]
391 impl ObjectStoreProvider for DummyProvider {
392 async fn new_store(
393 &self,
394 _base_path: Url,
395 _params: &ObjectStoreParams,
396 ) -> Result<ObjectStore> {
397 unreachable!("This test doesn't create stores")
398 }
399 }
400
401 #[test]
402 fn test_calculate_object_store_prefix() {
403 let provider = DummyProvider;
404 let url = Url::parse("dummy://blah/path").unwrap();
405 assert_eq!(
406 "dummy$blah",
407 provider.calculate_object_store_prefix(&url, None).unwrap()
408 );
409 }
410
411 #[tokio::test]
412 async fn test_get_store_resolves_base_scoped_options() {
413 use crate::object_store::StorageOptionsAccessor;
414
415 let registry = ObjectStoreRegistry::default();
416 let url = Url::parse("memory://test").unwrap();
417
418 let with_scoped = ObjectStoreParams {
419 storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options(
420 HashMap::from([
421 ("shared".to_string(), "value".to_string()),
422 ("base_1.account_key".to_string(), "base1-key".to_string()),
423 ]),
424 ))),
425 ..Default::default()
426 };
427 let without_scoped = ObjectStoreParams {
428 storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options(
429 HashMap::from([("shared".to_string(), "value".to_string())]),
430 ))),
431 ..Default::default()
432 };
433
434 let store_scoped = registry.get_store(url.clone(), &with_scoped).await.unwrap();
437 let store_plain = registry.get_store(url, &without_scoped).await.unwrap();
438 assert!(Arc::ptr_eq(&store_scoped, &store_plain));
439 }
440
441 #[test]
442 fn test_calculate_object_store_scheme_not_found() {
443 let registry = ObjectStoreRegistry::empty();
444 registry.insert("dummy", Arc::new(DummyProvider));
445 let s = "Invalid user input: No object store provider found for scheme: 'dummy2'\nValid schemes: dummy";
446 let result = registry
447 .calculate_object_store_prefix("dummy2://mybucket/my/long/path", None)
448 .expect_err("expected error")
449 .to_string();
450 assert_eq!(s, &result[..s.len()]);
451 }
452
453 #[test]
455 fn test_calculate_object_store_prefix_for_local() {
456 let registry = ObjectStoreRegistry::empty();
457 assert_eq!(
458 "file",
459 registry
460 .calculate_object_store_prefix("/tmp/foobar", None)
461 .unwrap()
462 );
463 }
464
465 #[test]
467 fn test_calculate_object_store_prefix_for_local_windows_path() {
468 let registry = ObjectStoreRegistry::empty();
469 assert_eq!(
470 "file",
471 registry
472 .calculate_object_store_prefix("c://dos/path", None)
473 .unwrap()
474 );
475 }
476
477 #[test]
479 fn test_calculate_object_store_prefix_for_dummy_path() {
480 let registry = ObjectStoreRegistry::empty();
481 registry.insert("dummy", Arc::new(DummyProvider));
482 assert_eq!(
483 "dummy$mybucket",
484 registry
485 .calculate_object_store_prefix("dummy://mybucket/my/long/path", None)
486 .unwrap()
487 );
488 }
489
490 #[tokio::test]
491 async fn test_stats_hit_miss_tracking() {
492 use crate::object_store::StorageOptionsAccessor;
493 let registry = ObjectStoreRegistry::default();
494 let url = Url::parse("memory://test").unwrap();
495
496 let params1 = ObjectStoreParams::default();
497 let params2 = ObjectStoreParams {
498 storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options(
499 HashMap::from([("k".into(), "v".into())]),
500 ))),
501 ..Default::default()
502 };
503
504 let cases: &[(&ObjectStoreParams, (u64, u64, usize))] = &[
506 (¶ms1, (0, 1, 1)), (¶ms1, (1, 1, 1)), (¶ms2, (1, 2, 2)), ];
510
511 let mut stores = vec![]; for (params, (hits, misses, active)) in cases {
513 stores.push(registry.get_store(url.clone(), params).await.unwrap());
514 let s = registry.stats();
515 assert_eq!(
516 (s.hits, s.misses, s.active_stores),
517 (*hits, *misses, *active)
518 );
519 }
520
521 assert!(Arc::ptr_eq(&stores[0], &stores[1]));
523 }
524}