This is an automated email from the ASF dual-hosted git repository.
alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs-object-store.git
The following commit(s) were added to refs/heads/main by this push:
new c2048e0 feat: add deregister to ObjectStoreRegistry (#784)
c2048e0 is described below
commit c2048e03380ca4656203ab0f0421873382ecf161
Author: Krisztián Szűcs <[email protected]>
AuthorDate: Wed Jul 8 20:23:53 2026 +0200
feat: add deregister to ObjectStoreRegistry (#784)
Add `ObjectStoreRegistry::deregister`, the inverse of `register`: it removes
the store registered at the exact scheme/authority/path of a URL and returns
it, pruning emptied tree branches. A default implementation returns `None`,
so
the addition is non-breaking for existing implementors.
---
src/registry.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 111 insertions(+)
diff --git a/src/registry.rs b/src/registry.rs
index 4794842..29282c1 100644
--- a/src/registry.rs
+++ b/src/registry.rs
@@ -82,6 +82,36 @@ pub trait ObjectStoreRegistry: Send + Sync + std::fmt::Debug
+ 'static {
/// assert!(Arc::ptr_eq(&ret, &bucket3));
/// ```
fn resolve(&self, url: &Url) -> crate::Result<(Arc<dyn ObjectStore>,
Path)>;
+
+ /// Deregister the store registered for exactly `url`, returning it if
present.
+ ///
+ /// Unlike [`resolve`](Self::resolve), which matches the longest registered
+ /// path that is a prefix of the object URL, this removes the store
+ /// registered at the *exact* scheme, authority and path of `url` (the
+ /// inverse of [`register`](Self::register)). A store registered at a
+ /// different path under the same authority is left in place.
+ ///
+ /// The default implementation does nothing and returns `None`.
+ ///
+ /// ```
+ /// # use std::sync::Arc;
+ /// # use url::Url;
+ /// # use object_store::memory::InMemory;
+ /// # use object_store::ObjectStore;
+ /// # use object_store::registry::{DefaultObjectStoreRegistry,
ObjectStoreRegistry};
+ /// #
+ /// let registry = DefaultObjectStoreRegistry::new();
+ /// let bucket = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
+ /// let url = Url::parse("s3://bucket/path").unwrap();
+ /// registry.register(url.clone(), bucket.clone());
+ ///
+ /// let removed = registry.deregister(&url).unwrap();
+ /// assert!(Arc::ptr_eq(&removed, &bucket));
+ /// ```
+ fn deregister(&self, url: &Url) -> Option<Arc<dyn ObjectStore>> {
+ let _ = url;
+ None
+ }
}
/// Error type for [`DefaultObjectStoreRegistry`]
@@ -164,6 +194,22 @@ impl PathEntry {
}
ret
}
+
+ /// Remove the store registered at the exact `segments` path under this
+ /// entry, returning it. Child entries left empty are pruned.
+ fn remove(&mut self, segments: &[&str]) -> Option<Arc<dyn ObjectStore>> {
+ match segments.split_first() {
+ None => self.store.take(),
+ Some((head, rest)) => {
+ let child = self.children.get_mut(*head)?;
+ let removed = child.remove(rest);
+ if child.store.is_none() && child.children.is_empty() {
+ self.children.remove(*head);
+ }
+ removed
+ }
+ }
+ }
}
impl DefaultObjectStoreRegistry {
@@ -215,6 +261,19 @@ impl ObjectStoreRegistry for DefaultObjectStoreRegistry {
Err(Error::NotFound.into())
}
+
+ fn deregister(&self, url: &Url) -> Option<Arc<dyn ObjectStore>> {
+ let mut map = self.map.write();
+ let key = url_key(url);
+ let segments: Vec<&str> = path_segments(url.path()).collect();
+ let entry = map.get_mut(key)?;
+ let removed = entry.remove(&segments);
+ // Drop the authority entry entirely once it holds no stores.
+ if entry.store.is_none() && entry.children.is_empty() {
+ map.remove(key);
+ }
+ removed
+ }
}
/// Extracts the scheme and authority of a URL (components before the Path)
@@ -343,4 +402,56 @@ mod tests {
assert_eq!(path.as_ref(), "6/7");
assert!(Arc::ptr_eq(&resolved, &custom_scheme));
}
+
+ #[test]
+ fn test_deregister() {
+ let registry = DefaultObjectStoreRegistry::new();
+ let a = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
+ let b = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
+
+ // Use a scheme `parse_url_opts` cannot lazily create, so a failed
+ // `resolve` reflects deregistration rather than the environment.
+ let a_url = Url::parse("mock://bucket/a").unwrap();
+ let b_url = Url::parse("mock://bucket/b").unwrap();
+ registry.register(a_url.clone(), Arc::clone(&a));
+ registry.register(b_url.clone(), Arc::clone(&b));
+
+ // Removes exactly the store at `a`, leaving its sibling `b` in place.
+ let removed = registry.deregister(&a_url).unwrap();
+ assert!(Arc::ptr_eq(&removed, &a));
+ assert!(registry.resolve(&a_url).is_err());
+ let (resolved, _) = registry.resolve(&b_url).unwrap();
+ assert!(Arc::ptr_eq(&resolved, &b));
+
+ // Deregistering again returns None.
+ assert!(registry.deregister(&a_url).is_none());
+ // A never-registered URL returns None.
+ assert!(
+ registry
+ .deregister(&Url::parse("mock://other/x").unwrap())
+ .is_none()
+ );
+ }
+
+ #[test]
+ fn test_deregister_keeps_longer_prefix() {
+ let registry = DefaultObjectStoreRegistry::new();
+ let outer = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
+ let inner = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
+ let outer_url = Url::parse("mock://bucket/a").unwrap();
+ let inner_url = Url::parse("mock://bucket/a/b").unwrap();
+ registry.register(outer_url.clone(), Arc::clone(&outer));
+ registry.register(inner_url.clone(), Arc::clone(&inner));
+
+ // Removing the outer mount leaves the nested one resolvable.
+ assert!(Arc::ptr_eq(
+ ®istry.deregister(&outer_url).unwrap(),
+ &outer
+ ));
+ let (resolved, path) = registry
+ .resolve(&Url::parse("mock://bucket/a/b/x").unwrap())
+ .unwrap();
+ assert!(Arc::ptr_eq(&resolved, &inner));
+ assert_eq!(path.as_ref(), "x");
+ }
}