From 31bcee9622b8fff44e9dae53e87cc5e9ce81ac04 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 29 Sep 2026 12:29:17 +0300 Subject: [PATCH] fix(dispatch): acquire GIL before AppState write lock to prevent deadlock ensure_compiled_snapshot, setup_database, and close_database took the AppState write lock and then called rebuild_snapshot(), which acquires the GIL to clone Py handles. When those ran on a tokio worker thread without the GIL already held, this created an AB-BA lock-order inversion against handle_rsgi (GIL held, then state.read()/write()): one thread could hold the Rust lock waiting for the GIL while another held the GIL waiting for the Rust lock, deadlocking the worker. Acquire the GIL first in all three call sites, then take the write lock inside that scope, matching the GIL-then-lock order used everywhere else in the hot path. Closes #205 --- src/dispatch.rs | 17 +++++++++++------ src/lib.rs | 15 ++++++++++----- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/dispatch.rs b/src/dispatch.rs index f5915e3..9176787 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -314,12 +314,17 @@ fn ensure_compiled_snapshot(state: &Arc>) -> Arc` handles, and taking a Rust lock first would let another thread hold the + // GIL while blocked on this same lock — classic AB-BA deadlock (issue #205). + Python::with_gil(|_py| { + let mut st = state.write(); + if st.compiled.is_none() { + st.compiled = Some(Arc::new(st.snapshot_routers())); + st.rebuild_snapshot(); + } + Arc::clone(st.compiled.as_ref().expect("just populated")) + }) } /// Synchronous RSGI handling for **openapi**, **404**, **405**, and **trivial matched routes**: diff --git a/src/lib.rs b/src/lib.rs index e195c34..587532e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -748,9 +748,14 @@ impl App { .map_err(|e| { pyo3::exceptions::PyRuntimeError::new_err(format!("DB connect error: {}", e)) })?; - let mut st = state.write(); - st.db_pool = Some(pool); - st.rebuild_snapshot(); + // GIL before the write lock (see the comment on `ensure_compiled_snapshot` in + // dispatch.rs / issue #205): this future runs on a tokio worker thread without + // the GIL, and `rebuild_snapshot` needs it to clone `Py` handles. + Python::with_gil(|_py| { + let mut st = state.write(); + st.db_pool = Some(pool); + st.rebuild_snapshot(); + }); Ok(()) }) } @@ -759,12 +764,12 @@ impl App { fn close_database<'py>(&self, py: Python<'py>) -> PyResult> { let state = self.state.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let pool = { + let pool = Python::with_gil(|_py| { let mut st = state.write(); let p = st.db_pool.take(); st.rebuild_snapshot(); p - }; + }); if let Some(p) = pool { p.close().await; }