From c4a1a0e5b9e9c88cbb62e6b38fbea676359c75f5 Mon Sep 17 00:00:00 2001 From: chitralverma Date: Mon, 13 Jul 2026 09:32:10 +0530 Subject: [PATCH 1/2] feat(core): expose registered schemes via OperatorRegistry::schemes Add a public OperatorRegistry::schemes() accessor that returns the set of registered schemes (HashSet) as a thin read over the existing factories map. For the global registry this is exactly the set that Operator::from_uri can construct. Enrich the from_uri 'scheme is not registered' error with an 'available' context listing the registered schemes, so a wrong dialect or missing services-* feature surfaces an actionable hint instead of a bare error. --- core/core/src/types/operator/registry.rs | 52 +++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/core/core/src/types/operator/registry.rs b/core/core/src/types/operator/registry.rs index d7ba45ff93c1..f40e62c87603 100644 --- a/core/core/src/types/operator/registry.rs +++ b/core/core/src/types/operator/registry.rs @@ -18,7 +18,7 @@ use crate::types::builder::{Builder, Configurator}; use crate::types::{IntoOperatorUri, OperatorUri}; use crate::{Error, ErrorKind, Operator, Result}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::{LazyLock, Mutex}; /// Factory signature used to construct [`Operator`] from a URI and extra options. @@ -59,6 +59,27 @@ impl OperatorRegistry { guard.insert(key, factory::); } + /// Return the set of schemes currently registered. + /// + /// For the global registry returned by [`OperatorRegistry::get`], this is + /// exactly the set of schemes that [`Operator::from_uri`] can construct, as + /// determined by the compiled-in `services-*` features. + /// + /// ``` + /// use opendal_core::OperatorRegistry; + /// + /// let schemes = OperatorRegistry::get().schemes(); + /// assert!(schemes.contains("memory")); + /// ``` + pub fn schemes(&self) -> HashSet { + self.factories + .lock() + .expect("operator registry mutex poisoned") + .keys() + .cloned() + .collect() + } + /// Load an [`Operator`] via the factory registered for the URI's scheme. pub fn load(&self, uri: impl IntoOperatorUri) -> Result { let parsed = uri.into_operator_uri()?; @@ -71,8 +92,11 @@ impl OperatorRegistry { .get(scheme) .copied() .ok_or_else(|| { + let mut available: Vec = self.schemes().into_iter().collect(); + available.sort(); Error::new(ErrorKind::Unsupported, "scheme is not registered") .with_context("scheme", scheme.to_string()) + .with_context("available", available.join(", ")) })?; factory(&parsed) @@ -84,3 +108,29 @@ fn factory(uri: &OperatorUri) -> Result { let cfg = C::from_uri(uri)?; Operator::from_config(cfg) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn global_registry_exposes_memory_scheme() { + let schemes = OperatorRegistry::get().schemes(); + assert!(schemes.contains("memory")); + } + + #[test] + fn unregistered_scheme_error_lists_available_schemes() { + let err = OperatorRegistry::get() + .load("no-such-scheme://bucket/path") + .unwrap_err(); + + assert_eq!(err.kind(), ErrorKind::Unsupported); + + let msg = err.to_string(); + assert!(msg.contains("scheme is not registered")); + assert!(msg.contains("no-such-scheme")); + assert!(msg.contains("available")); + assert!(msg.contains("memory")); + } +} From fab712d99f20c9b3d46db68f8328b0ea4205c453 Mon Sep 17 00:00:00 2001 From: chitralverma Date: Mon, 13 Jul 2026 10:12:22 +0530 Subject: [PATCH 2/2] fix(core): avoid self-deadlock in registry load() on unsupported scheme load() held the factories mutex while the ok_or_else closure called schemes(), which re-locked the same non-reentrant mutex and deadlocked on the unsupported-scheme path. Collect the available schemes from the guard already held instead of re-locking. --- core/core/src/types/operator/registry.rs | 34 +++++++++++++++--------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/core/core/src/types/operator/registry.rs b/core/core/src/types/operator/registry.rs index f40e62c87603..cbce30701ce2 100644 --- a/core/core/src/types/operator/registry.rs +++ b/core/core/src/types/operator/registry.rs @@ -85,19 +85,27 @@ impl OperatorRegistry { let parsed = uri.into_operator_uri()?; let scheme = parsed.scheme(); - let factory = self - .factories - .lock() - .expect("operator registry mutex poisoned") - .get(scheme) - .copied() - .ok_or_else(|| { - let mut available: Vec = self.schemes().into_iter().collect(); - available.sort(); - Error::new(ErrorKind::Unsupported, "scheme is not registered") - .with_context("scheme", scheme.to_string()) - .with_context("available", available.join(", ")) - })?; + let factory = { + let guard = self + .factories + .lock() + .expect("operator registry mutex poisoned"); + + match guard.get(scheme).copied() { + Some(factory) => factory, + None => { + // Collect the available schemes from the guard we already hold + // to avoid re-locking the mutex (which would self-deadlock). + let mut available: Vec = guard.keys().cloned().collect(); + available.sort(); + return Err( + Error::new(ErrorKind::Unsupported, "scheme is not registered") + .with_context("scheme", scheme.to_string()) + .with_context("available", available.join(", ")), + ); + } + } + }; factory(&parsed) }