clawhdf5-remote: ObjectStoreStorage works from any thread

It refused whenever Handle::try_current() was Ok, which is also the case
inside spawn_blocking threads — so the workaround its own error message
recommended failed the same way, and the backend could only be used from
a bare std::thread in a tokio application.

Reads are now spawned on the storage's own runtime and the caller waits on
a channel: the future never runs on the caller's thread, so neither a
spawn_blocking thread nor a current-thread runtime can deadlock or panic
(a read inside a runtime blocks that thread, like any blocking call; the
docs still recommend spawn_blocking there).

Tests: a read in spawn_blocking of a multi-thread runtime and a read inside
a current-thread runtime's task give File::open's values (both errors
before).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 18:35:36 -05:00
co-authored by Claude Opus 5.5
parent 30a1ed6b9c
commit c5b2afbc35
5 changed files with 75 additions and 29 deletions
+19 -16
View File
@@ -3,12 +3,11 @@
//!
//! `object_store` is async and [`Storage`] is synchronous (parsing is CPU
//! work; `docs/design/range-reads.md` §3 (a)). The storage owns a small
//! multi-threaded tokio runtime (two worker threads) and blocks the calling
//! thread on it for each read, so it can be used from ordinary threads —
//! several at once. Calling it from inside another tokio runtime would
//! block that runtime's worker, so it refuses with
//! [`RemoteError::Usage`]: from async code, read in
//! `tokio::task::spawn_blocking`.
//! multi-threaded tokio runtime (two worker threads): each read is spawned
//! on it and the calling thread waits for the result, so it can be used
//! from any thread — several at once, and from async code too. From async
//! code prefer `tokio::task::spawn_blocking` (a read blocks the thread it
//! is called on, which inside a runtime is one of its workers).
//!
//! The object is pinned when the storage is made: its size, and its ETag
//! (sent as `If-Match` with every read, and compared with every response)
@@ -105,19 +104,23 @@ impl ObjectStoreStorage {
)
}
fn block_on<T>(
/// Run `fut` on the storage's own runtime and wait for it. The future
/// never runs on the caller's thread, so the caller's context does not
/// matter: a plain thread, `spawn_blocking`, or even inside another
/// runtime (whose thread is then blocked for the duration of the read,
/// as by any blocking call, but nothing deadlocks or panics).
fn block_on<T: Send + 'static>(
&self,
fut: impl std::future::Future<Output = Result<T, RemoteError>>,
fut: impl std::future::Future<Output = Result<T, RemoteError>> + Send + 'static,
) -> Result<T, RemoteError> {
if tokio::runtime::Handle::try_current().is_ok() {
return Err(RemoteError::Usage(
"ObjectStoreStorage blocks on its own runtime and cannot be read from inside \
an async runtime; read in tokio::task::spawn_blocking"
.into(),
));
}
let rt = self.runtime.as_ref().expect("runtime lives until drop");
rt.block_on(fut)
let (tx, rx) = std::sync::mpsc::sync_channel(1);
rt.spawn(async move {
let _ = tx.send(fut.await);
});
rx.recv().map_err(|_| {
RemoteError::ObjectStore("the object store task ended without a result".into())
})?
}
fn options(&self, range: Range<u64>) -> GetOptions {