mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-22 05:50:30 +00:00
Add tokio runtime and GIL-releasing block_on helpers
The Postgres driver is async but the Python-facing API will be synchronous, so we need a way to drive tokio futures to completion from a sync, GIL-holding method. Add a process-global, lazily-initialised multi-thread tokio runtime (`database::runtime`) whose worker threads only drive the connection tasks: the actual blocking wait happens on the calling Python thread, so a small fixed pool can't starve itself. Add the `block_on` / `block_on_result` / `block_on_next` extension traits (`database::postgres::helpers`) that block on that runtime while releasing the GIL (`py.detach`), so other Python threads keep running while we wait. `pg_err_to_py` maps a driver error into a Python `RuntimeError`. `BlockingPostgresStream` is implemented generically over `Pin<&mut Fuse<S>>` rather than just `RowStream`. This is deliberate: it lets the cursor state machine added in the next change be unit-tested against an in-memory fake stream, with no live database. The module doc spells out why polling a `RowStream` off the runtime (the non-blocking fast path) is sound, and why the `Fuse` bound is required rather than merely assumed. These modules are `pub` for now so the not-yet-consumed items don't trip clippy's `dead_code` lint; later changes tighten that. Unit tests cover the ready / pending / exhausted stream paths (using an in-memory stream and a `yield_now`-based pending future) and the runtime's shared-instance behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ce4719542c
commit
7e8da7619d
@@ -0,0 +1,49 @@
|
||||
//! Shared tokio runtime used by the async-backed database drivers (currently
|
||||
//! just the Postgres backend). The runtime is created lazily on first use and
|
||||
//! lives for the lifetime of the process.
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
static RUNTIME: Lazy<Runtime> = Lazy::new(|| {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
// A small fixed pool is enough: these threads only drive the I/O-bound
|
||||
// connection tasks and the futures blocked on them (the actual blocking
|
||||
// wait happens on the calling Python thread, see `helpers::block_on`),
|
||||
// so we don't need one worker per connection.
|
||||
//
|
||||
// This is also why a fixed pool of two can't deadlock: every blocking
|
||||
// wait (`Runtime::block_on`) runs on the *calling* Python thread, never
|
||||
// on a worker, so the workers are always free to drive the connection
|
||||
// tasks that those waits are waiting on. Moving a blocking wait onto a
|
||||
// worker (e.g. via `Handle::block_on` from inside the runtime) would
|
||||
// break that invariant.
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.thread_name("synapse-db")
|
||||
.build()
|
||||
.expect("failed to build tokio runtime for synapse database backend")
|
||||
});
|
||||
|
||||
pub fn runtime() -> &'static Runtime {
|
||||
&RUNTIME
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn runtime_runs_futures_and_is_shared() {
|
||||
// The runtime drives a future to completion...
|
||||
assert_eq!(runtime().block_on(async { 1 + 1 }), 2);
|
||||
|
||||
// ...and is the same instance on every call (it's a process-global
|
||||
// `Lazy`), so a second `block_on` reuses it rather than spinning up a
|
||||
// fresh runtime.
|
||||
let first = runtime() as *const Runtime;
|
||||
let second = runtime() as *const Runtime;
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(runtime().block_on(async { "ok" }), "ok");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user