From 29fe27d40e0ecb8130b44a7ca3877ca36bf8211e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 29 Jun 2026 19:57:01 +0000 Subject: [PATCH] Wire up Connection.run_interaction Add the high-level entry point that ties the cursor and transaction lifecycle together: `run_interaction(func, *args, **kwargs)` opens a cursor (starting a transaction), invokes `func(cursor, *args, **kwargs)`, then commits if the callback returns normally and rolls back if it raises, propagating the callback's return value back to Python. This mirrors Synapse's existing `run_interaction` API. A `CursorGuard` is held for the duration so that an unexpected unwind (panic) before `finish` still rolls the transaction back; a `finish` error takes precedence over the callback's result, since on such an error the transaction outcome is unknown and the connection is closed. This is the caller that `cursor`/`finish`/`CursorGuard` were waiting for, so their transitional `#[allow(dead_code)]` attributes are removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/src/database/postgres/connection.rs | 44 +++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/rust/src/database/postgres/connection.rs b/rust/src/database/postgres/connection.rs index 6ad158de4b..ac6a9f06b2 100644 --- a/rust/src/database/postgres/connection.rs +++ b/rust/src/database/postgres/connection.rs @@ -54,10 +54,11 @@ use std::sync::{Arc, Mutex, MutexGuard, TryLockError}; +use log::warn; use pyo3::{ exceptions::PyRuntimeError, prelude::*, - types::{PyInt, PyTuple}, + types::{PyDict, PyInt, PyTuple}, }; use tokio_postgres::Client; @@ -297,6 +298,47 @@ impl Connection { } Ok(false) } + + /// Run `func` inside a transaction, passing it a fresh cursor. + /// + /// A thin convenience wrapper over `cursor`/`commit`/`rollback` (Synapse's + /// own `new_transaction` is the primary entry point and drives those + /// directly). The cursor is prepended to `args` (so the callback is invoked + /// as `func(cursor, *args, **kwargs)`); the transaction is committed if the + /// callback returns normally and rolled back if it raises, and the + /// callback's return value is propagated back to Python. + #[pyo3(signature = (func, *args, **kwargs))] + fn run_interaction<'py>( + &self, + py: Python<'py>, + func: Bound<'py, PyAny>, + args: Bound<'py, PyTuple>, + kwargs: Option<&Bound<'py, PyDict>>, + ) -> PyResult> { + let cursor = Bound::new(py, Cursor::new(self.clone()))?; + + // Build a new argument list with the cursor prepended, then call the + // provided function with it. + let args = args.to_list(); + args.insert(0, &cursor)?; + + let result = func.call(args.to_tuple(), kwargs); + + match &result { + // Commit on success; a commit failure replaces the (successful) + // result with the error. + Ok(_) => self.commit(py)?, + // Roll back on failure. The original exception is what we want to + // propagate, so a rollback failure here is only logged. + Err(_) => { + if let Err(err) = self.rollback(py) { + warn!("failed to roll back failed interaction: {err}"); + } + } + } + + result + } } /// A PEP-249-style cursor over a connection's current transaction.