PyO3 Async Runtime Bridge#
Module: python/src/runtime.rs
LanceDB's Python bindings (PyO3) need a Tokio async runtime to drive Rust futures from Python. Rather than using pyo3_async_runtimes::tokio directly, the codebase provides a custom fork-safe wrapper: LanceRuntime.
The Problem: pyo3_async_runtimes::tokio + fork()#
pyo3_async_runtimes::tokio stores its multi-threaded Tokio runtime in a OnceLock — once initialized it can never be replaced. Tokio's worker threads do not survive fork(), so a child process that inherits the locked runtime will hang permanently on every future_into_py call.
This affects any Python code using multiprocessing with the default fork start method.
The Solution: LanceRuntime#
LanceRuntime is a pyo3_async_runtimes::generic::Runtime implementation backed by an AtomicPtr<runtime::Runtime> instead of a OnceLock. The key components are:
RUNTIME: AtomicPtr— stores a raw pointer to the heap-allocated Tokio runtime.get_runtime()— lazily initializes the runtime on first access using a spin-lock idiom withRUNTIME_INSTALLING.atfork_child()— apthread_atforkchild handler that nulls the pointer without dropping the old runtime (dropping would try to join the now-dead threads and hang). The nextspawnthen creates a fresh runtime in the child.install_atfork()— registersatfork_childwithlibc::pthread_atforkonce. No-ops on Windows.
The Tokio runtime itself is a standard multi-thread runtime with all features enabled, named "lancedb-tokio-worker".
Task-Local Context#
LanceRuntime implements ContextExt using a tokio::task_local! cell to propagate Python TaskLocals across async boundaries.
Public API#
Two functions are the sole interface for the rest of the python/src/ codebase:
| Function | Usage |
|---|---|
future_into_py(py, fut) | Converts an async Rust future into a Python awaitable. Drop-in for pyo3_async_runtimes::tokio::future_into_py. Used in #[pymethods]. |
block_on(fut) | Blocks the calling thread on a future. Used in sync #[pyfunction]s. Must not be called from within Tokio worker threads. |
Usage in the Codebase#
future_into_py is the dominant pattern, wrapping nearly every async operation exposed to Python. It is imported via use crate::runtime::future_into_py and used in connection.rs, table.rs, and query.rs — for example:
- Connection operations (
connect,open_table,create_table,table_names, namespace management, jobs, etc.) all return Python awaitables viafuture_into_py. - Every
#[pymethods]block onConnectionpassesself_.py()plus a closure returningPyResult<T>.
block_on is used sparingly where sync execution is unavoidable:
connect_namespace_clientandconnect_namespace— synchronous#[pyfunction]s that build namespace database connections.
Dependencies#
Relevant entries in python/Cargo.toml:
pyo3 = "0.28"withextension-module,abi3-py39pyo3-async-runtimes = "0.28"withattributes,tokio-runtimefeaturestokio = "1.40"withsync,rt-multi-threadlibc = "0.2"forpthread_atfork
Architecture Context#
The Python SDK is a native-binding wrapper over rust/lancedb/ compiled via PyO3. All async Rust operations in the core library are driven through this single runtime bridge.
Key Files#
| File | Role |
|---|---|
python/src/runtime.rs | Full runtime implementation |
python/src/connection.rs | Primary consumer of future_into_py and block_on |
python/Cargo.toml | Dependency declarations |