Skip to main content

hf_hub/
blocking.rs

1use std::future::Future;
2use std::sync::Arc;
3
4use crate::client::HFClient;
5use crate::error::{HFError, HFResult};
6use crate::repository::{HFRepository, RepoType, RepoTypeDataset, RepoTypeKernel, RepoTypeModel, RepoTypeSpace};
7
8/// A dedicated background thread owning a single-threaded tokio runtime,
9/// modeled on `reqwest::blocking`.
10///
11/// The runtime never leaves this thread: `Runtime::block_on` and `Runtime`'s
12/// `Drop` both panic inside another tokio runtime, so keeping the runtime off
13/// caller threads removes both panic modes.
14///
15/// The thread parks in `runtime.block_on(shutdown_rx)`, driving IO, timers,
16/// and spawned tasks. When the last handle drops, the shutdown sender drops,
17/// `block_on` returns, and the runtime is dropped on its own thread. The
18/// thread is detached so dropping the last handle never blocks.
19pub(crate) struct RuntimeThread {
20    handle: tokio::runtime::Handle,
21    _shutdown: futures::channel::oneshot::Sender<()>,
22}
23
24impl RuntimeThread {
25    /// Spawns the background thread and waits for its runtime to come up.
26    fn spawn() -> HFResult<Arc<Self>> {
27        let (handle_tx, handle_rx) = std::sync::mpsc::sync_channel(1);
28        let (shutdown_tx, shutdown_rx) = futures::channel::oneshot::channel::<()>();
29        std::thread::Builder::new()
30            .name("hf-hub-blocking-runtime".to_string())
31            .spawn(move || {
32                let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
33                    Ok(runtime) => runtime,
34                    Err(e) => {
35                        let _ = handle_tx.send(Err(e));
36                        return;
37                    },
38                };
39                let _ = handle_tx.send(Ok(runtime.handle().clone()));
40                runtime.block_on(async {
41                    let _ = shutdown_rx.await;
42                });
43            })
44            .map_err(|e| HFError::Other(format!("Failed to spawn tokio runtime thread: {e}")))?;
45        handle_rx
46            .recv()
47            .map_err(|_| HFError::Other("tokio runtime thread exited before initializing".to_string()))?
48            .map_err(|e| HFError::Other(format!("Failed to create tokio runtime: {e}")))
49            .map(|handle| {
50                Arc::new(Self {
51                    handle,
52                    _shutdown: shutdown_tx,
53                })
54            })
55    }
56
57    /// Runs `future` on the background runtime, blocking the caller until it
58    /// completes.
59    ///
60    /// Safe inside another tokio runtime: the future is polled on a
61    /// short-lived scoped thread via `Handle::block_on`, so no tokio blocking
62    /// API runs on the caller thread. The scoped thread also lets `future`
63    /// borrow from the caller's stack — no `'static` bound. Panics are
64    /// resumed on the caller.
65    ///
66    /// Do not call from the background runtime thread itself (e.g. a progress
67    /// callback): that deadlocks, since the driver is parked inside this call.
68    pub(crate) fn block_on<F>(&self, future: F) -> F::Output
69    where
70        F: Future + Send,
71        F::Output: Send,
72    {
73        std::thread::scope(|scope| {
74            scope
75                .spawn(|| self.handle.block_on(future))
76                .join()
77                .unwrap_or_else(|panic| std::panic::resume_unwind(panic))
78        })
79    }
80}
81
82/// Synchronous/blocking counterpart to [`HFClient`].
83///
84/// Wraps an [`HFClient`] together with a single-threaded tokio runtime on a
85/// dedicated background thread (`RuntimeThread` in this module), so the async
86/// API can be used from synchronous code — including from inside another
87/// tokio runtime, where a caller-owned runtime would panic.
88///
89/// Xet uploads and downloads do not run on this runtime: hf-xet requires a
90/// multi-threaded runtime with the IO and time drivers enabled, so the
91/// single-threaded runtime here does not meet its requirements. When a Xet
92/// transfer is triggered through any blocking handle, hf-xet spins up its
93/// own multi-threaded thread pool to back the `XetSession`, separate from
94/// the runtime thread owned by `HFClientSync`.
95///
96/// See [`HFClient`] for configuration and API semantics.
97#[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
98#[derive(Clone)]
99pub struct HFClientSync {
100    pub(crate) inner: HFClient,
101    pub(crate) runtime: Arc<RuntimeThread>,
102}
103
104/// Synchronous/blocking counterpart to [`HFRepository`], parameterized by the repo kind via `T`.
105///
106/// Wraps an [`HFRepository<T>`] and blocks on the corresponding async methods.
107///
108/// See [`HFRepository`] for method semantics.
109#[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
110pub struct HFRepositorySync<T: RepoType> {
111    pub(crate) inner: Arc<HFRepository<T>>,
112    pub(crate) runtime: Arc<RuntimeThread>,
113}
114
115impl<T: RepoType> Clone for HFRepositorySync<T> {
116    fn clone(&self) -> Self {
117        Self {
118            inner: Arc::clone(&self.inner),
119            runtime: Arc::clone(&self.runtime),
120        }
121    }
122}
123
124/// Synchronous/blocking counterpart to [`crate::buckets::HFBucket`].
125///
126/// Wraps an [`crate::buckets::HFBucket`] and blocks on the corresponding async
127/// methods.
128///
129/// See [`crate::buckets::HFBucket`] for method semantics.
130#[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
131#[derive(Clone)]
132pub struct HFBucketSync {
133    pub(crate) inner: Arc<crate::buckets::HFBucket>,
134    pub(crate) runtime: Arc<RuntimeThread>,
135}
136
137impl std::fmt::Debug for HFClientSync {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        f.debug_struct("HFClientSync").finish()
140    }
141}
142
143impl<T: RepoType> std::fmt::Debug for HFRepositorySync<T> {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        f.debug_struct("HFRepositorySync").field("inner", &self.inner).finish()
146    }
147}
148
149impl std::fmt::Debug for HFBucketSync {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        f.debug_struct("HFBucketSync").field("inner", &self.inner).finish()
152    }
153}
154
155impl HFClientSync {
156    /// Creates an `HFClientSync` using the default configuration from the environment.
157    ///
158    /// Reads the standard environment variables used by [`HFClient::new`].
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the tokio runtime cannot be created or if `HFClient::new` fails.
163    pub fn new() -> HFResult<Self> {
164        Ok(Self {
165            inner: HFClient::new()?,
166            runtime: RuntimeThread::spawn()?,
167        })
168    }
169
170    /// Creates an `HFClientSync` from an existing [`HFClient`].
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the tokio runtime cannot be created.
175    pub fn from_inner(inner: HFClient) -> HFResult<Self> {
176        Ok(Self {
177            inner,
178            runtime: RuntimeThread::spawn()?,
179        })
180    }
181
182    /// Creates a blocking handle for any repo kind via a turbofished generic.
183    ///
184    /// See [`HFClient::repository`].
185    pub fn repository<T: RepoType>(
186        &self,
187        repo_type: T,
188        owner: impl Into<String>,
189        name: impl Into<String>,
190    ) -> HFRepositorySync<T> {
191        HFRepositorySync::new(self.clone(), repo_type, owner, name)
192    }
193
194    /// Creates a blocking handle for a model repository.
195    ///
196    /// See [`HFClient::model`].
197    pub fn model(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepositorySync<RepoTypeModel> {
198        self.repository(RepoTypeModel, owner, name)
199    }
200
201    /// Creates a blocking handle for a dataset repository.
202    ///
203    /// See [`HFClient::dataset`].
204    pub fn dataset(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepositorySync<RepoTypeDataset> {
205        self.repository(RepoTypeDataset, owner, name)
206    }
207
208    /// Creates a blocking handle for a Space repository.
209    ///
210    /// See [`HFClient::space`].
211    pub fn space(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepositorySync<RepoTypeSpace> {
212        self.repository(RepoTypeSpace, owner, name)
213    }
214
215    /// Creates a blocking handle for a kernel repository.
216    ///
217    /// See [`HFClient::kernel`].
218    pub fn kernel(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepositorySync<RepoTypeKernel> {
219        self.repository(RepoTypeKernel, owner, name)
220    }
221
222    /// Creates a blocking handle for a bucket.
223    ///
224    /// See [`HFClient::bucket`].
225    pub fn bucket(&self, owner: impl Into<String>, name: impl Into<String>) -> HFBucketSync {
226        HFBucketSync::new(self.clone(), owner, name)
227    }
228}
229
230impl<T: RepoType> HFRepositorySync<T> {
231    /// Creates a blocking repository handle.
232    ///
233    /// See [`HFRepository::new`].
234    pub fn new(client: HFClientSync, repo_type: T, owner: impl Into<String>, name: impl Into<String>) -> Self {
235        Self {
236            inner: Arc::new(HFRepository::new(client.inner.clone(), repo_type, owner, name)),
237            runtime: client.runtime.clone(),
238        }
239    }
240
241    /// The repository owner (user or organization name).
242    pub fn owner(&self) -> &str {
243        self.inner.owner()
244    }
245
246    /// The repository name (without the owner prefix).
247    pub fn name(&self) -> &str {
248        self.inner.name()
249    }
250
251    /// The full `"owner/name"` identifier used in Hub API calls.
252    ///
253    /// If no owner is set, returns just the name (for repos using short-form IDs like `"gpt2"`).
254    pub fn repo_path(&self) -> String {
255        self.inner.repo_path()
256    }
257
258    /// The marker for this handle's repo kind. Call
259    /// [`RepoType::singular`] / [`RepoType::plural`] / [`RepoType::url_prefix`] on it
260    /// to get the corresponding string.
261    pub fn repo_type(&self) -> &T {
262        self.inner.repo_type()
263    }
264}
265
266impl HFBucketSync {
267    /// Creates a blocking bucket handle.
268    ///
269    /// See [`crate::buckets::HFBucket::new`].
270    pub fn new(client: HFClientSync, owner: impl Into<String>, name: impl Into<String>) -> Self {
271        Self {
272            inner: Arc::new(crate::buckets::HFBucket::new(client.inner.clone(), owner, name)),
273            runtime: client.runtime.clone(),
274        }
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_hfapisync_creation() {
284        let client = HFClientSync::new();
285        assert!(client.is_ok());
286    }
287
288    #[test]
289    fn test_hfapisync_from_api() {
290        let async_client = HFClient::builder().build().unwrap();
291        let client = HFClientSync::from_inner(async_client);
292        assert!(client.is_ok());
293    }
294
295    #[test]
296    fn test_sync_repo_constructors() {
297        let client = HFClientSync::from_inner(HFClient::builder().build().unwrap()).unwrap();
298        let repo = client.model("openai-community", "gpt2");
299        let space = client.space("huggingface", "transformers-benchmarks");
300
301        assert_eq!(repo.owner(), "openai-community");
302        assert_eq!(repo.name(), "gpt2");
303        assert_eq!(repo.repo_type().singular(), "model");
304        assert_eq!(space.repo_type().singular(), "space");
305    }
306
307    /// Endpoint that refuses connections: bind an ephemeral port, then free it.
308    fn unreachable_endpoint() -> String {
309        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
310        let port = listener.local_addr().unwrap().port();
311        drop(listener);
312        format!("http://127.0.0.1:{port}")
313    }
314
315    fn client_with_unreachable_endpoint() -> HFClientSync {
316        let inner = HFClient::builder()
317            .endpoint(unreachable_endpoint())
318            .retry_max_attempts(0)
319            .build()
320            .unwrap();
321        HFClientSync::from_inner(inner).unwrap()
322    }
323
324    /// Regression: panicked with "Cannot start a runtime from within a
325    /// runtime" when the caller-side handle owned the runtime. The connection
326    /// error is expected; the point is completing without a panic.
327    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
328    async fn sync_call_inside_multi_thread_runtime_does_not_panic() {
329        let client = client_with_unreachable_endpoint();
330        let result = client.model("openai-community", "gpt2").info().send();
331        assert!(result.is_err());
332    }
333
334    #[tokio::test]
335    async fn sync_call_inside_current_thread_runtime_does_not_panic() {
336        let client = client_with_unreachable_endpoint();
337        let result = client.model("openai-community", "gpt2").info().send();
338        assert!(result.is_err());
339    }
340
341    /// Regression: dropping the handle from async context also panicked when
342    /// the caller side owned the runtime.
343    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
344    async fn construct_and_drop_inside_async_context_does_not_panic() {
345        let client = HFClientSync::from_inner(HFClient::builder().build().unwrap()).unwrap();
346        drop(client);
347    }
348
349    #[test]
350    fn clones_share_runtime_thread_and_survive_staggered_drops() {
351        let client = client_with_unreachable_endpoint();
352        let clone = client.clone();
353        let repo = client.model("openai-community", "gpt2");
354        assert!(Arc::ptr_eq(&client.runtime, &clone.runtime));
355        assert!(Arc::ptr_eq(&client.runtime, &repo.runtime));
356
357        drop(client);
358        drop(clone);
359        let result = repo.exists().send();
360        assert!(result.is_err());
361    }
362
363    #[test]
364    fn block_on_resumes_task_panics_on_the_caller() {
365        let runtime = RuntimeThread::spawn().unwrap();
366        let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
367            runtime.block_on(async {
368                panic!("boom");
369            })
370        }));
371        let panic = caught.unwrap_err();
372        assert_eq!(panic.downcast_ref::<&str>(), Some(&"boom"));
373    }
374
375    /// `block_on` must accept borrowing (non-`'static`) futures — the
376    /// property that keeps the wrapper call sites unchanged.
377    #[test]
378    fn block_on_accepts_borrowing_futures() {
379        let runtime = RuntimeThread::spawn().unwrap();
380        let data = String::from("borrowed");
381        let borrowed = &data;
382        let len = runtime.block_on(async move { borrowed.len() });
383        assert_eq!(len, data.len());
384    }
385}